| 1 | #--------------------------------------------------------------------------- |
| 2 | # Name: wxPython.lib.evtmgr |
| 3 | # Purpose: An easier, more "Pythonic" and more OO method of registering |
| 4 | # handlers for wxWindows events using the Publish/Subscribe |
| 5 | # pattern. |
| 6 | # |
| 7 | # Author: Robb Shecter and Robin Dunn |
| 8 | # |
| 9 | # Created: 12-December-2002 |
| 10 | # RCS-ID: $Id$ |
| 11 | # Copyright: (c) 2003 by db-X Corporation |
| 12 | # Licence: wxWindows license |
| 13 | #--------------------------------------------------------------------------- |
| 14 | # 12/02/2003 - Jeff Grimmett (grimmtooth@softhome.net) |
| 15 | # |
| 16 | # o Updated for 2.5 compatability. |
| 17 | # |
| 18 | |
| 19 | """ |
| 20 | A module that allows multiple handlers to respond to single wxWindows |
| 21 | events. This allows true NxN Observer/Observable connections: One |
| 22 | event can be received by multiple handlers, and one handler can |
| 23 | receive multiple events. |
| 24 | |
| 25 | There are two ways to register event handlers. The first way is |
| 26 | similar to standard wxPython handler registration: |
| 27 | |
| 28 | from wxPython.lib.evtmgr import eventManager |
| 29 | eventManager.Register(handleEvents, EVT_BUTTON, win=frame, id=101) |
| 30 | |
| 31 | There's also a new object-oriented way to register for events. This |
| 32 | invocation is equivalent to the one above, but does not require the |
| 33 | programmer to declare or track control ids or parent containers: |
| 34 | |
| 35 | eventManager.Register(handleEvents, EVT_BUTTON, myButton) |
| 36 | |
| 37 | This module is Python 2.1+ compatible. |
| 38 | |
| 39 | """ |
| 40 | import wx |
| 41 | import pubsub # publish / subscribe library |
| 42 | |
| 43 | #--------------------------------------------------------------------------- |
| 44 | |
| 45 | |
| 46 | class EventManager: |
| 47 | """ |
| 48 | This is the main class in the module, and is the only class that |
| 49 | the application programmer needs to use. There is a pre-created |
| 50 | instance of this class called 'eventManager'. It should not be |
| 51 | necessary to create other instances. |
| 52 | """ |
| 53 | def __init__(self): |
| 54 | self.eventAdapterDict = {} |
| 55 | self.messageAdapterDict = {} |
| 56 | self.windowTopicLookup = {} |
| 57 | self.listenerTopicLookup = {} |
| 58 | self.__publisher = pubsub.Publisher() |
| 59 | self.EMPTY_LIST = [] |
| 60 | |
| 61 | |
| 62 | def Register(self, listener, event, source=None, win=None, id=None): |
| 63 | """ |
| 64 | Registers a listener function (or any callable object) to |
| 65 | receive events of type event coming from the source window. |
| 66 | For example:: |
| 67 | eventManager.Register(self.OnButton, EVT_BUTTON, theButton) |
| 68 | |
| 69 | Alternatively, the specific window where the event is |
| 70 | delivered, and/or the ID of the event source can be specified. |
| 71 | For example:: |
| 72 | eventManager.Register(self.OnButton, EVT_BUTTON, win=self, id=ID_BUTTON) |
| 73 | |
| 74 | or:: |
| 75 | eventManager.Register(self.OnButton, EVT_BUTTON, theButton, self) |
| 76 | |
| 77 | """ |
| 78 | |
| 79 | # 1. Check if the 'event' is actually one of the multi- |
| 80 | # event macros. |
| 81 | if _macroInfo.isMultiEvent(event): |
| 82 | raise 'Cannot register the macro, '+`event`+'. Register instead the individual events.' |
| 83 | |
| 84 | # Support a more OO API. This allows the GUI widget itself to |
| 85 | # be specified, and the id to be retrieved from the system, |
| 86 | # instead of kept track of explicitly by the programmer. |
| 87 | # (Being used to doing GUI work with Java, this seems to me to be |
| 88 | # the natural way of doing things.) |
| 89 | if source is not None: |
| 90 | id = source.GetId() |
| 91 | |
| 92 | if win is None: |
| 93 | # Some widgets do not function as their own windows. |
| 94 | win = self._determineWindow(source) |
| 95 | |
| 96 | topic = (event, win, id) |
| 97 | |
| 98 | # Create an adapter from the PS system back to wxEvents, and |
| 99 | # possibly one from wxEvents: |
| 100 | if not self.__haveMessageAdapter(listener, topic): |
| 101 | messageAdapter = MessageAdapter(eventHandler=listener, topicPattern=topic) |
| 102 | try: |
| 103 | self.messageAdapterDict[topic][listener] = messageAdapter |
| 104 | except KeyError: |
| 105 | self.messageAdapterDict[topic] = {} |
| 106 | self.messageAdapterDict[topic][listener] = messageAdapter |
| 107 | |
| 108 | if not self.eventAdapterDict.has_key(topic): |
| 109 | self.eventAdapterDict[topic] = EventAdapter(event, win, id) |
| 110 | else: |
| 111 | # Throwing away a duplicate request |
| 112 | pass |
| 113 | |
| 114 | # For time efficiency when deregistering by window: |
| 115 | try: |
| 116 | self.windowTopicLookup[win].append(topic) |
| 117 | except KeyError: |
| 118 | self.windowTopicLookup[win] = [] |
| 119 | self.windowTopicLookup[win].append(topic) |
| 120 | |
| 121 | # For time efficiency when deregistering by listener: |
| 122 | try: |
| 123 | self.listenerTopicLookup[listener].append(topic) |
| 124 | except KeyError: |
| 125 | self.listenerTopicLookup[listener] = [] |
| 126 | self.listenerTopicLookup[listener].append(topic) |
| 127 | |
| 128 | # See if the source understands the listeningFor protocol. |
| 129 | # This is a bit of a test I'm working on - it allows classes |
| 130 | # to know when their events are being listened to. I use |
| 131 | # it to enable chaining events from contained windows only |
| 132 | # when needed. |
| 133 | if source is not None: |
| 134 | try: |
| 135 | # Let the source know that we're listening for this |
| 136 | # event. |
| 137 | source.listeningFor(event) |
| 138 | except AttributeError: |
| 139 | pass |
| 140 | |
| 141 | # Some aliases for Register, just for kicks |
| 142 | Bind = Register |
| 143 | Subscribe = Register |
| 144 | |
| 145 | |
| 146 | def DeregisterWindow(self, win): |
| 147 | """ |
| 148 | Deregister all events coming from the given window. |
| 149 | """ |
| 150 | win = self._determineWindow(win) |
| 151 | topics = self.__getTopics(win) |
| 152 | |
| 153 | if topics: |
| 154 | for aTopic in topics: |
| 155 | self.__deregisterTopic(aTopic) |
| 156 | |
| 157 | del self.windowTopicLookup[win] |
| 158 | |
| 159 | |
| 160 | def DeregisterListener(self, listener): |
| 161 | """ |
| 162 | Deregister all event notifications for the given listener. |
| 163 | """ |
| 164 | try: |
| 165 | topicList = self.listenerTopicLookup[listener] |
| 166 | except KeyError: |
| 167 | return |
| 168 | |
| 169 | for topic in topicList: |
| 170 | topicDict = self.messageAdapterDict[topic] |
| 171 | |
| 172 | if topicDict.has_key(listener): |
| 173 | topicDict[listener].Destroy() |
| 174 | del topicDict[listener] |
| 175 | |
| 176 | if len(topicDict) == 0: |
| 177 | self.eventAdapterDict[topic].Destroy() |
| 178 | del self.eventAdapterDict[topic] |
| 179 | del self.messageAdapterDict[topic] |
| 180 | |
| 181 | del self.listenerTopicLookup[listener] |
| 182 | |
| 183 | |
| 184 | def GetStats(self): |
| 185 | """ |
| 186 | Return a dictionary with data about my state. |
| 187 | """ |
| 188 | stats = {} |
| 189 | stats['Adapters: Message'] = reduce(lambda x,y: x+y, [0] + map(len, self.messageAdapterDict.values())) |
| 190 | stats['Adapters: Event'] = len(self.eventAdapterDict) |
| 191 | stats['Topics: Total'] = len(self.__getTopics()) |
| 192 | stats['Topics: Dead'] = len(self.GetDeadTopics()) |
| 193 | return stats |
| 194 | |
| 195 | |
| 196 | def DeregisterDeadTopics(self): |
| 197 | """ |
| 198 | Deregister any entries relating to dead |
| 199 | wxPython objects. Not sure if this is an |
| 200 | important issue; 1) My app code always de-registers |
| 201 | listeners it doesn't need. 2) I don't think |
| 202 | that lingering references to these dead objects |
| 203 | is a problem. |
| 204 | """ |
| 205 | for topic in self.GetDeadTopics(): |
| 206 | self.__deregisterTopic(topic) |
| 207 | |
| 208 | |
| 209 | def GetDeadTopics(self): |
| 210 | """ |
| 211 | Return a list of topics relating to dead wxPython |
| 212 | objects. |
| 213 | """ |
| 214 | return filter(self.__isDeadTopic, self.__getTopics()) |
| 215 | |
| 216 | |
| 217 | def __winString(self, aWin): |
| 218 | """ |
| 219 | A string rep of a window for debugging |
| 220 | """ |
| 221 | try: |
| 222 | name = aWin.GetClassName() |
| 223 | i = id(aWin) |
| 224 | return '%s #%d' % (name, i) |
| 225 | except wx.PyDeadObjectError: |
| 226 | return '(dead wx.Object)' |
| 227 | |
| 228 | |
| 229 | def __topicString(self, aTopic): |
| 230 | """ |
| 231 | A string rep of a topic for debugging |
| 232 | """ |
| 233 | return '[%-26s %s]' % (aTopic[0].__name__, self.winString(aTopic[1])) |
| 234 | |
| 235 | |
| 236 | def __listenerString(self, aListener): |
| 237 | """ |
| 238 | A string rep of a listener for debugging |
| 239 | """ |
| 240 | try: |
| 241 | return aListener.im_class.__name__ + '.' + aListener.__name__ |
| 242 | except: |
| 243 | return 'Function ' + aListener.__name__ |
| 244 | |
| 245 | |
| 246 | def __deregisterTopic(self, aTopic): |
| 247 | try: |
| 248 | messageAdapterList = self.messageAdapterDict[aTopic].values() |
| 249 | except KeyError: |
| 250 | # This topic isn't valid. Probably because it was deleted |
| 251 | # by listener. |
| 252 | return |
| 253 | |
| 254 | for messageAdapter in messageAdapterList: |
| 255 | messageAdapter.Destroy() |
| 256 | |
| 257 | self.eventAdapterDict[aTopic].Destroy() |
| 258 | del self.messageAdapterDict[aTopic] |
| 259 | del self.eventAdapterDict[aTopic] |
| 260 | |
| 261 | |
| 262 | def __getTopics(self, win=None): |
| 263 | if win is None: |
| 264 | return self.messageAdapterDict.keys() |
| 265 | |
| 266 | if win is not None: |
| 267 | try: |
| 268 | return self.windowTopicLookup[win] |
| 269 | except KeyError: |
| 270 | return self.EMPTY_LIST |
| 271 | |
| 272 | |
| 273 | def __isDeadWxObject(self, anObject): |
| 274 | return isinstance(anObject, wx._wxPyDeadObject) |
| 275 | |
| 276 | |
| 277 | def __isDeadTopic(self, aTopic): |
| 278 | return self.__isDeadWxObject(aTopic[1]) |
| 279 | |
| 280 | |
| 281 | def __haveMessageAdapter(self, eventHandler, topicPattern): |
| 282 | """ |
| 283 | Return True if there's already a message adapter |
| 284 | with these specs. |
| 285 | """ |
| 286 | try: |
| 287 | return self.messageAdapterDict[topicPattern].has_key(eventHandler) |
| 288 | except KeyError: |
| 289 | return 0 |
| 290 | |
| 291 | |
| 292 | def _determineWindow(self, aComponent): |
| 293 | """ |
| 294 | Return the window that corresponds to this component. |
| 295 | A window is something that supports the Connect protocol. |
| 296 | Most things registered with the event manager are a window, |
| 297 | but there are apparently some exceptions. If more are |
| 298 | discovered, the implementation can be changed to a dictionary |
| 299 | lookup along the lines of class : function-to-get-window. |
| 300 | """ |
| 301 | if isinstance(aComponent, wx.MenuItem): |
| 302 | return aComponent.GetMenu() |
| 303 | else: |
| 304 | return aComponent |
| 305 | |
| 306 | |
| 307 | |
| 308 | #--------------------------------------------------------------------------- |
| 309 | # From here down is implementaion and support classes, although you may |
| 310 | # find some of them useful in other contexts. |
| 311 | #--------------------------------------------------------------------------- |
| 312 | |
| 313 | |
| 314 | class EventMacroInfo: |
| 315 | """ |
| 316 | A class that provides information about event macros. |
| 317 | """ |
| 318 | def __init__(self): |
| 319 | self.lookupTable = {} |
| 320 | |
| 321 | |
| 322 | def getEventTypes(self, eventMacro): |
| 323 | """ |
| 324 | Return the list of event types that the given |
| 325 | macro corresponds to. |
| 326 | """ |
| 327 | try: |
| 328 | return self.lookupTable[eventMacro] |
| 329 | except KeyError: |
| 330 | win = FakeWindow() |
| 331 | try: |
| 332 | eventMacro(win, None, None) |
| 333 | except (TypeError, AssertionError): |
| 334 | eventMacro(win, None) |
| 335 | self.lookupTable[eventMacro] = win.eventTypes |
| 336 | return win.eventTypes |
| 337 | |
| 338 | |
| 339 | def eventIsA(self, event, macroList): |
| 340 | """ |
| 341 | Return True if the event is one of the given |
| 342 | macros. |
| 343 | """ |
| 344 | eventType = event.GetEventType() |
| 345 | for macro in macroList: |
| 346 | if eventType in self.getEventTypes(macro): |
| 347 | return 1 |
| 348 | return 0 |
| 349 | |
| 350 | |
| 351 | def macroIsA(self, macro, macroList): |
| 352 | """ |
| 353 | Return True if the macro is in the macroList. |
| 354 | The added value of this method is that it takes |
| 355 | multi-events into account. The macroList parameter |
| 356 | will be coerced into a sequence if needed. |
| 357 | """ |
| 358 | if callable(macroList): |
| 359 | macroList = (macroList,) |
| 360 | testList = self.getEventTypes(macro) |
| 361 | eventList = [] |
| 362 | for m in macroList: |
| 363 | eventList.extend(self.getEventTypes(m)) |
| 364 | # Return True if every element in testList is in eventList |
| 365 | for element in testList: |
| 366 | if element not in eventList: |
| 367 | return 0 |
| 368 | return 1 |
| 369 | |
| 370 | |
| 371 | def isMultiEvent(self, macro): |
| 372 | """ |
| 373 | Return True if the given macro actually causes |
| 374 | multiple events to be registered. |
| 375 | """ |
| 376 | return len(self.getEventTypes(macro)) > 1 |
| 377 | |
| 378 | |
| 379 | #--------------------------------------------------------------------------- |
| 380 | |
| 381 | class FakeWindow: |
| 382 | """ |
| 383 | Used internally by the EventMacroInfo class. The FakeWindow is |
| 384 | the most important component of the macro-info utility: it |
| 385 | implements the Connect() protocol of wxWindow, but instead of |
| 386 | registering for events, it keeps track of what parameters were |
| 387 | passed to it. |
| 388 | """ |
| 389 | def __init__(self): |
| 390 | self.eventTypes = [] |
| 391 | |
| 392 | def Connect(self, id1, id2, eventType, handlerFunction): |
| 393 | self.eventTypes.append(eventType) |
| 394 | |
| 395 | |
| 396 | #--------------------------------------------------------------------------- |
| 397 | |
| 398 | class EventAdapter: |
| 399 | """ |
| 400 | A class that adapts incoming wxWindows events to |
| 401 | Publish/Subscribe messages. |
| 402 | |
| 403 | In other words, this is the object that's seen by the |
| 404 | wxWindows system. Only one of these registers for any |
| 405 | particular wxWindows event. It then relays it into the |
| 406 | PS system, which lets many listeners respond. |
| 407 | """ |
| 408 | def __init__(self, func, win, id): |
| 409 | """ |
| 410 | Instantiate a new adapter. Pre-compute my Publish/Subscribe |
| 411 | topic, which is constant, and register with wxWindows. |
| 412 | """ |
| 413 | self.publisher = pubsub.Publisher() |
| 414 | self.topic = ((func, win, id),) |
| 415 | self.id = id |
| 416 | self.win = win |
| 417 | self.eventType = _macroInfo.getEventTypes(func)[0] |
| 418 | |
| 419 | # Register myself with the wxWindows event system |
| 420 | try: |
| 421 | func(win, id, self.handleEvent) |
| 422 | self.callStyle = 3 |
| 423 | except (TypeError, AssertionError): |
| 424 | func(win, self.handleEvent) |
| 425 | self.callStyle = 2 |
| 426 | |
| 427 | |
| 428 | def disconnect(self): |
| 429 | if self.callStyle == 3: |
| 430 | return self.win.Disconnect(self.id, -1, self.eventType) |
| 431 | else: |
| 432 | return self.win.Disconnect(-1, -1, self.eventType) |
| 433 | |
| 434 | |
| 435 | def handleEvent(self, event): |
| 436 | """ |
| 437 | In response to a wxWindows event, send a PS message |
| 438 | """ |
| 439 | self.publisher.sendMessage(topic=self.topic, data=event) |
| 440 | |
| 441 | |
| 442 | def Destroy(self): |
| 443 | try: |
| 444 | if not self.disconnect(): |
| 445 | print 'disconnect failed' |
| 446 | except wx.PyDeadObjectError: |
| 447 | print 'disconnect failed: dead object' ##???? |
| 448 | |
| 449 | |
| 450 | #--------------------------------------------------------------------------- |
| 451 | |
| 452 | class MessageAdapter: |
| 453 | """ |
| 454 | A class that adapts incoming Publish/Subscribe messages |
| 455 | to wxWindows event calls. |
| 456 | |
| 457 | This class works opposite the EventAdapter, and |
| 458 | retrieves the information an EventAdapter has sent in a message. |
| 459 | Strictly speaking, this class is not required: Event listeners |
| 460 | could pull the original wxEvent object out of the PS Message |
| 461 | themselves. |
| 462 | |
| 463 | However, by pairing an instance of this class with each wxEvent |
| 464 | handler, the handlers can use the standard API: they receive an |
| 465 | event as a parameter. |
| 466 | """ |
| 467 | def __init__(self, eventHandler, topicPattern): |
| 468 | """ |
| 469 | Instantiate a new MessageAdapter that send wxEvents to the |
| 470 | given eventHandler. |
| 471 | """ |
| 472 | self.eventHandler = eventHandler |
| 473 | pubsub.Publisher().subscribe(listener=self.deliverEvent, topic=(topicPattern,)) |
| 474 | |
| 475 | def deliverEvent(self, message): |
| 476 | event = message.data # Extract the wxEvent |
| 477 | self.eventHandler(event) # Perform the call as wxWindows would |
| 478 | |
| 479 | def Destroy(self): |
| 480 | pubsub.Publisher().unsubscribe(listener=self.deliverEvent) |
| 481 | |
| 482 | |
| 483 | #--------------------------------------------------------------------------- |
| 484 | # Create globals |
| 485 | |
| 486 | _macroInfo = EventMacroInfo() |
| 487 | |
| 488 | # For now a singleton is not enforced. Should it be or can we trust |
| 489 | # the programmers? |
| 490 | eventManager = EventManager() |
| 491 | |
| 492 | |
| 493 | #--------------------------------------------------------------------------- |
| 494 | # simple test code |
| 495 | |
| 496 | |
| 497 | if __name__ == '__main__': |
| 498 | app = wx.PySimpleApp() |
| 499 | frame = wx.Frame(None, -1, 'Event Test', size=(300,300)) |
| 500 | button = wx.ToggleButton(frame, -1, 'Listen for Mouse Events') |
| 501 | sizer = wx.BoxSizer(wx.HORIZONTAL) |
| 502 | sizer.Add(button, 0, 0 | wx.ALL, 10) |
| 503 | frame.SetAutoLayout(1) |
| 504 | frame.SetSizer(sizer) |
| 505 | |
| 506 | # |
| 507 | # Demonstrate 1) register/deregister, 2) Multiple listeners receiving |
| 508 | # one event, and 3) Multiple events going to one listener. |
| 509 | # |
| 510 | |
| 511 | def printEvent(event): |
| 512 | print 'Name:',event.GetClassName(),'Timestamp',event.GetTimestamp() |
| 513 | |
| 514 | def enableFrameEvents(event): |
| 515 | # Turn the output of mouse events on and off |
| 516 | if event.IsChecked(): |
| 517 | print '\nEnabling mouse events...' |
| 518 | eventManager.Register(printEvent, wx.EVT_MOTION, frame) |
| 519 | eventManager.Register(printEvent, wx.EVT_LEFT_DOWN, frame) |
| 520 | else: |
| 521 | print '\nDisabling mouse events...' |
| 522 | eventManager.DeregisterWindow(frame) |
| 523 | |
| 524 | # Send togglebutton events to both the on/off code as well |
| 525 | # as the function that prints to stdout. |
| 526 | eventManager.Register(printEvent, wx.EVT_TOGGLEBUTTON, button) |
| 527 | eventManager.Register(enableFrameEvents, wx.EVT_TOGGLEBUTTON, button) |
| 528 | |
| 529 | frame.CenterOnScreen() |
| 530 | frame.Show(1) |
| 531 | app.MainLoop() |