]>
git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/evtmgr.py
   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 
   7 # Author:      Robb Shecter and Robin Dunn 
   9 # Created:     12-December-2002 
  11 # Copyright:   (c) 2003 by db-X Corporation 
  12 # Licence:     wxWindows license 
  13 #--------------------------------------------------------------------------- 
  14 # 12/02/2003 - Jeff Grimmett (grimmtooth@softhome.net) 
  16 # o Updated for 2.5 compatability. 
  20 A module that allows multiple handlers to respond to single wxWidgets 
  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. 
  25 There are two ways to register event handlers.  The first way is 
  26 similar to standard wxPython handler registration:: 
  28     from wx.lib.evtmgr import eventManager 
  29     eventManager.Register(handleEvents, EVT_BUTTON, win=frame, id=101) 
  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:: 
  35     eventManager.Register(handleEvents, EVT_BUTTON, myButton) 
  37 This module is Python 2.1+ compatible. 
  41 import  pubsub 
# publish / subscribe library 
  43 #--------------------------------------------------------------------------- 
  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. 
  54         self
.eventAdapterDict    
= {} 
  55         self
.messageAdapterDict  
= {} 
  56         self
.windowTopicLookup   
= {} 
  57         self
.listenerTopicLookup 
= {} 
  58         self
.__publisher         
= pubsub
.Publisher() 
  62     def Register(self
, listener
, event
, source
=None, win
=None, id=None): 
  64         Registers a listener function (or any callable object) to 
  65         receive events of type event coming from the source window. 
  68             eventManager.Register(self.OnButton, EVT_BUTTON, theButton) 
  70         Alternatively, the specific window where the event is 
  71         delivered, and/or the ID of the event source can be specified. 
  74             eventManager.Register(self.OnButton, EVT_BUTTON, win=self, id=ID_BUTTON) 
  78             eventManager.Register(self.OnButton, EVT_BUTTON, theButton, self) 
  82         # 1. Check if the 'event' is actually one of the multi- 
  84         if _macroInfo
.isMultiEvent(event
): 
  85             raise 'Cannot register the macro, '+`event`
+'.  Register instead the individual events.' 
  87         # Support a more OO API.  This allows the GUI widget itself to 
  88         # be specified, and the id to be retrieved from the system, 
  89         # instead of kept track of explicitly by the programmer. 
  90         # (Being used to doing GUI work with Java, this seems to me to be 
  91         # the natural way of doing things.) 
  92         if source 
is not None: 
  96             # Some widgets do not function as their own windows. 
  97             win 
= self
._determineWindow
(source
) 
  99         topic 
= (event
, win
, id) 
 101         #  Create an adapter from the PS system back to wxEvents, and 
 102         #  possibly one from wxEvents: 
 103         if not self
.__haveMessageAdapter
(listener
, topic
): 
 104             messageAdapter 
= MessageAdapter(eventHandler
=listener
, topicPattern
=topic
) 
 106                 self
.messageAdapterDict
[topic
][listener
] = messageAdapter
 
 108                 self
.messageAdapterDict
[topic
] = {} 
 109                 self
.messageAdapterDict
[topic
][listener
] = messageAdapter
 
 111             if not self
.eventAdapterDict
.has_key(topic
): 
 112                 self
.eventAdapterDict
[topic
] = EventAdapter(event
, win
, id) 
 114             # Throwing away a duplicate request 
 117         # For time efficiency when deregistering by window: 
 119             self
.windowTopicLookup
[win
].append(topic
) 
 121             self
.windowTopicLookup
[win
] = [] 
 122             self
.windowTopicLookup
[win
].append(topic
) 
 124         # For time efficiency when deregistering by listener: 
 126             self
.listenerTopicLookup
[listener
].append(topic
) 
 128             self
.listenerTopicLookup
[listener
] = [] 
 129             self
.listenerTopicLookup
[listener
].append(topic
) 
 131         # See if the source understands the listeningFor protocol. 
 132         # This is a bit of a test I'm working on - it allows classes 
 133         # to know when their events are being listened to.  I use 
 134         # it to enable chaining events from contained windows only 
 136         if source 
is not None: 
 138                 # Let the source know that we're listening  for this 
 140                 source
.listeningFor(event
) 
 141             except AttributeError: 
 144     # Some aliases for Register, just for kicks 
 149     def DeregisterWindow(self
, win
): 
 151         Deregister all events coming from the given window. 
 153         win    
= self
._determineWindow
(win
) 
 154         topics 
= self
.__getTopics
(win
) 
 157             for aTopic 
in topics
: 
 158                 self
.__deregisterTopic
(aTopic
) 
 160             del self
.windowTopicLookup
[win
] 
 163     def DeregisterListener(self
, listener
): 
 165         Deregister all event notifications for the given listener. 
 168             topicList 
= self
.listenerTopicLookup
[listener
] 
 172         for topic 
in topicList
: 
 173             topicDict 
= self
.messageAdapterDict
[topic
] 
 175             if topicDict
.has_key(listener
): 
 176                 topicDict
[listener
].Destroy() 
 177                 del topicDict
[listener
] 
 179                 if len(topicDict
) == 0: 
 180                     self
.eventAdapterDict
[topic
].Destroy() 
 181                     del self
.eventAdapterDict
[topic
] 
 182                     del self
.messageAdapterDict
[topic
] 
 184         del self
.listenerTopicLookup
[listener
] 
 189         Return a dictionary with data about my state. 
 192         stats
['Adapters: Message'] = reduce(lambda x
,y
: x
+y
, [0] + map(len, self
.messageAdapterDict
.values())) 
 193         stats
['Adapters: Event']   = len(self
.eventAdapterDict
) 
 194         stats
['Topics: Total']     = len(self
.__getTopics
()) 
 195         stats
['Topics: Dead']      = len(self
.GetDeadTopics()) 
 199     def DeregisterDeadTopics(self
): 
 201         Deregister any entries relating to dead 
 202         wxPython objects.  Not sure if this is an 
 203         important issue; 1) My app code always de-registers 
 204         listeners it doesn't need.  2) I don't think 
 205         that lingering references to these dead objects 
 208         for topic 
in self
.GetDeadTopics(): 
 209             self
.__deregisterTopic
(topic
) 
 212     def GetDeadTopics(self
): 
 214         Return a list of topics relating to dead wxPython 
 217         return filter(self
.__isDeadTopic
, self
.__getTopics
()) 
 220     def __winString(self
, aWin
): 
 222         A string rep of a window for debugging 
 225             name 
= aWin
.GetClassName() 
 227             return '%s #%d' % (name
, i
) 
 228         except wx
.PyDeadObjectError
: 
 229             return '(dead wx.Object)' 
 232     def __topicString(self
, aTopic
): 
 234         A string rep of a topic for debugging 
 236         return '[%-26s %s]' % (aTopic
[0].__name
__, self
.winString(aTopic
[1])) 
 239     def __listenerString(self
, aListener
): 
 241         A string rep of a listener for debugging 
 244             return aListener
.im_class
.__name
__ + '.' + aListener
.__name
__ 
 246             return 'Function ' + aListener
.__name
__ 
 249     def __deregisterTopic(self
, aTopic
): 
 251             messageAdapterList 
= self
.messageAdapterDict
[aTopic
].values() 
 253             # This topic isn't valid.  Probably because it was deleted 
 257         for messageAdapter 
in messageAdapterList
: 
 258             messageAdapter
.Destroy() 
 260         self
.eventAdapterDict
[aTopic
].Destroy() 
 261         del self
.messageAdapterDict
[aTopic
] 
 262         del self
.eventAdapterDict
[aTopic
] 
 265     def __getTopics(self
, win
=None): 
 267             return self
.messageAdapterDict
.keys() 
 271                 return self
.windowTopicLookup
[win
] 
 273                 return self
.EMPTY_LIST
 
 276     def __isDeadWxObject(self
, anObject
): 
 277         return isinstance(anObject
, wx
._core
._wxPyDeadObject
) 
 280     def __isDeadTopic(self
, aTopic
): 
 281         return self
.__isDeadWxObject
(aTopic
[1]) 
 284     def __haveMessageAdapter(self
, eventHandler
, topicPattern
): 
 286         Return True if there's already a message adapter 
 290             return self
.messageAdapterDict
[topicPattern
].has_key(eventHandler
) 
 295     def _determineWindow(self
, aComponent
): 
 297         Return the window that corresponds to this component. 
 298         A window is something that supports the Connect protocol. 
 299         Most things registered with the event manager are a window, 
 300         but there are apparently some exceptions.  If more are 
 301         discovered, the implementation can be changed to a dictionary 
 302         lookup along the lines of class : function-to-get-window. 
 304         if isinstance(aComponent
, wx
.MenuItem
): 
 305             return aComponent
.GetMenu() 
 311 #--------------------------------------------------------------------------- 
 312 # From here down is implementaion and support classes, although you may 
 313 # find some of them useful in other contexts. 
 314 #--------------------------------------------------------------------------- 
 317 class EventMacroInfo
: 
 319     A class that provides information about event macros. 
 322         self
.lookupTable 
= {} 
 325     def getEventTypes(self
, eventMacro
): 
 327         Return the list of event types that the given 
 328         macro corresponds to. 
 331             return self
.lookupTable
[eventMacro
] 
 335                 eventMacro(win
, None, None) 
 336             except (TypeError, AssertionError): 
 337                 eventMacro(win
, None) 
 338             self
.lookupTable
[eventMacro
] = win
.eventTypes
 
 339             return win
.eventTypes
 
 342     def eventIsA(self
, event
, macroList
): 
 344         Return True if the event is one of the given 
 347         eventType 
= event
.GetEventType() 
 348         for macro 
in macroList
: 
 349             if eventType 
in self
.getEventTypes(macro
): 
 354     def macroIsA(self
, macro
, macroList
): 
 356         Return True if the macro is in the macroList. 
 357         The added value of this method is that it takes 
 358         multi-events into account.  The macroList parameter 
 359         will be coerced into a sequence if needed. 
 361         if callable(macroList
): 
 362             macroList 
= (macroList
,) 
 363         testList  
= self
.getEventTypes(macro
) 
 366             eventList
.extend(self
.getEventTypes(m
)) 
 367         # Return True if every element in testList is in eventList 
 368         for element 
in testList
: 
 369             if element 
not in eventList
: 
 374     def isMultiEvent(self
, macro
): 
 376         Return True if the given macro actually causes 
 377         multiple events to be registered. 
 379         return len(self
.getEventTypes(macro
)) > 1 
 382 #--------------------------------------------------------------------------- 
 386     Used internally by the EventMacroInfo class.  The FakeWindow is 
 387     the most important component of the macro-info utility: it 
 388     implements the Connect() protocol of wxWindow, but instead of 
 389     registering for events, it keeps track of what parameters were 
 395     def Connect(self
, id1
, id2
, eventType
, handlerFunction
): 
 396         self
.eventTypes
.append(eventType
) 
 399 #--------------------------------------------------------------------------- 
 403     A class that adapts incoming wxWindows events to 
 404     Publish/Subscribe messages. 
 406     In other words, this is the object that's seen by the 
 407     wxWindows system.  Only one of these registers for any 
 408     particular wxWindows event.  It then relays it into the 
 409     PS system, which lets many listeners respond. 
 411     def __init__(self
, func
, win
, id): 
 413         Instantiate a new adapter. Pre-compute my Publish/Subscribe 
 414         topic, which is constant, and register with wxWindows. 
 416         self
.publisher 
= pubsub
.Publisher() 
 417         self
.topic     
= ((func
, win
, id),) 
 420         self
.eventType 
= _macroInfo
.getEventTypes(func
)[0] 
 422         # Register myself with the wxWindows event system 
 424             func(win
, id, self
.handleEvent
) 
 426         except (TypeError, AssertionError): 
 427             func(win
, self
.handleEvent
) 
 431     def disconnect(self
): 
 432         if self
.callStyle 
== 3: 
 433             return self
.win
.Disconnect(self
.id, -1, self
.eventType
) 
 435             return self
.win
.Disconnect(-1, -1, self
.eventType
) 
 438     def handleEvent(self
, event
): 
 440         In response to a wxWindows event, send a PS message 
 442         self
.publisher
.sendMessage(topic
=self
.topic
, data
=event
) 
 447             if not self
.disconnect(): 
 448                 print 'disconnect failed' 
 449         except wx
.PyDeadObjectError
: 
 450             print 'disconnect failed: dead object'              ##???? 
 453 #--------------------------------------------------------------------------- 
 455 class MessageAdapter
: 
 457     A class that adapts incoming Publish/Subscribe messages 
 458     to wxWindows event calls. 
 460     This class works opposite the EventAdapter, and 
 461     retrieves the information an EventAdapter has sent in a message. 
 462     Strictly speaking, this class is not required: Event listeners 
 463     could pull the original wxEvent object out of the PS Message 
 466     However, by pairing an instance of this class with each wxEvent 
 467     handler, the handlers can use the standard API: they receive an 
 468     event as a parameter. 
 470     def __init__(self
, eventHandler
, topicPattern
): 
 472         Instantiate a new MessageAdapter that send wxEvents to the 
 475         self
.eventHandler 
= eventHandler
 
 476         pubsub
.Publisher().subscribe(listener
=self
.deliverEvent
, topic
=(topicPattern
,)) 
 478     def deliverEvent(self
, message
): 
 479         event 
= message
.data        
# Extract the wxEvent 
 480         self
.eventHandler(event
)    # Perform the call as wxWindows would 
 483         pubsub
.Publisher().unsubscribe(listener
=self
.deliverEvent
) 
 486 #--------------------------------------------------------------------------- 
 489 _macroInfo    
= EventMacroInfo() 
 491 # For now a singleton is not enforced.  Should it be or can we trust 
 493 eventManager  
= EventManager() 
 496 #--------------------------------------------------------------------------- 
 500 if __name__ 
== '__main__': 
 501     app    
= wx
.PySimpleApp() 
 502     frame  
= wx
.Frame(None, -1, 'Event Test', size
=(300,300)) 
 503     button 
= wx
.ToggleButton(frame
, -1, 'Listen for Mouse Events') 
 504     sizer  
= wx
.BoxSizer(wx
.HORIZONTAL
) 
 505     sizer
.Add(button
, 0, 0 | wx
.ALL
, 10) 
 506     frame
.SetAutoLayout(1) 
 507     frame
.SetSizer(sizer
) 
 510     # Demonstrate 1) register/deregister, 2) Multiple listeners receiving 
 511     # one event, and 3) Multiple events going to one listener. 
 514     def printEvent(event
): 
 515         print 'Name:',event
.GetClassName(),'Timestamp',event
.GetTimestamp() 
 517     def enableFrameEvents(event
): 
 518         # Turn the output of mouse events on and off 
 519         if event
.IsChecked(): 
 520             print '\nEnabling mouse events...' 
 521             eventManager
.Register(printEvent
, wx
.EVT_MOTION
,    frame
) 
 522             eventManager
.Register(printEvent
, wx
.EVT_LEFT_DOWN
, frame
) 
 524             print '\nDisabling mouse events...' 
 525             eventManager
.DeregisterWindow(frame
) 
 527     # Send togglebutton events to both the on/off code as well 
 528     # as the function that prints to stdout. 
 529     eventManager
.Register(printEvent
,        wx
.EVT_TOGGLEBUTTON
, button
) 
 530     eventManager
.Register(enableFrameEvents
, wx
.EVT_TOGGLEBUTTON
, button
) 
 532     frame
.CenterOnScreen()