]>
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 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. 
  25 There are two ways to register event handlers.  The first way is 
  26 similar to standard wxPython handler registration: 
  28     from wxPython.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. 
  67             eventManager.Register(self.OnButton, EVT_BUTTON, theButton) 
  69         Alternatively, the specific window where the event is 
  70         delivered, and/or the ID of the event source can be specified. 
  72             eventManager.Register(self.OnButton, EVT_BUTTON, win=self, id=ID_BUTTON) 
  75             eventManager.Register(self.OnButton, EVT_BUTTON, theButton, self) 
  79         # 1. Check if the 'event' is actually one of the multi- 
  81         if _macroInfo
.isMultiEvent(event
): 
  82             raise 'Cannot register the macro, '+`event`
+'.  Register instead the individual events.' 
  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: 
  93             # Some widgets do not function as their own windows. 
  94             win 
= self
._determineWindow
(source
) 
  96         topic 
= (event
, win
, id) 
  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
) 
 103                 self
.messageAdapterDict
[topic
][listener
] = messageAdapter
 
 105                 self
.messageAdapterDict
[topic
] = {} 
 106                 self
.messageAdapterDict
[topic
][listener
] = messageAdapter
 
 108             if not self
.eventAdapterDict
.has_key(topic
): 
 109                 self
.eventAdapterDict
[topic
] = EventAdapter(event
, win
, id) 
 111             # Throwing away a duplicate request 
 114         # For time efficiency when deregistering by window: 
 116             self
.windowTopicLookup
[win
].append(topic
) 
 118             self
.windowTopicLookup
[win
] = [] 
 119             self
.windowTopicLookup
[win
].append(topic
) 
 121         # For time efficiency when deregistering by listener: 
 123             self
.listenerTopicLookup
[listener
].append(topic
) 
 125             self
.listenerTopicLookup
[listener
] = [] 
 126             self
.listenerTopicLookup
[listener
].append(topic
) 
 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 
 133         if source 
is not None: 
 135                 # Let the source know that we're listening  for this 
 137                 source
.listeningFor(event
) 
 138             except AttributeError: 
 141     # Some aliases for Register, just for kicks 
 146     def DeregisterWindow(self
, win
): 
 148         Deregister all events coming from the given window. 
 150         win    
= self
._determineWindow
(win
) 
 151         topics 
= self
.__getTopics
(win
) 
 154             for aTopic 
in topics
: 
 155                 self
.__deregisterTopic
(aTopic
) 
 157             del self
.windowTopicLookup
[win
] 
 160     def DeregisterListener(self
, listener
): 
 162         Deregister all event notifications for the given listener. 
 165             topicList 
= self
.listenerTopicLookup
[listener
] 
 169         for topic 
in topicList
: 
 170             topicDict 
= self
.messageAdapterDict
[topic
] 
 172             if topicDict
.has_key(listener
): 
 173                 topicDict
[listener
].Destroy() 
 174                 del topicDict
[listener
] 
 176                 if len(topicDict
) == 0: 
 177                     self
.eventAdapterDict
[topic
].Destroy() 
 178                     del self
.eventAdapterDict
[topic
] 
 179                     del self
.messageAdapterDict
[topic
] 
 181         del self
.listenerTopicLookup
[listener
] 
 186         Return a dictionary with data about my state. 
 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()) 
 196     def DeregisterDeadTopics(self
): 
 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 
 205         for topic 
in self
.GetDeadTopics(): 
 206             self
.__deregisterTopic
(topic
) 
 209     def GetDeadTopics(self
): 
 211         Return a list of topics relating to dead wxPython 
 214         return filter(self
.__isDeadTopic
, self
.__getTopics
()) 
 217     def __winString(self
, aWin
): 
 219         A string rep of a window for debugging 
 222             name 
= aWin
.GetClassName() 
 224             return '%s #%d' % (name
, i
) 
 225         except wx
.PyDeadObjectError
: 
 226             return '(dead wx.Object)' 
 229     def __topicString(self
, aTopic
): 
 231         A string rep of a topic for debugging 
 233         return '[%-26s %s]' % (aTopic
[0].__name
__, self
.winString(aTopic
[1])) 
 236     def __listenerString(self
, aListener
): 
 238         A string rep of a listener for debugging 
 241             return aListener
.im_class
.__name
__ + '.' + aListener
.__name
__ 
 243             return 'Function ' + aListener
.__name
__ 
 246     def __deregisterTopic(self
, aTopic
): 
 248             messageAdapterList 
= self
.messageAdapterDict
[aTopic
].values() 
 250             # This topic isn't valid.  Probably because it was deleted 
 254         for messageAdapter 
in messageAdapterList
: 
 255             messageAdapter
.Destroy() 
 257         self
.eventAdapterDict
[aTopic
].Destroy() 
 258         del self
.messageAdapterDict
[aTopic
] 
 259         del self
.eventAdapterDict
[aTopic
] 
 262     def __getTopics(self
, win
=None): 
 264             return self
.messageAdapterDict
.keys() 
 268                 return self
.windowTopicLookup
[win
] 
 270                 return self
.EMPTY_LIST
 
 273     def __isDeadWxObject(self
, anObject
): 
 274         return isinstance(anObject
, wx
._wxPyDeadObject
) 
 277     def __isDeadTopic(self
, aTopic
): 
 278         return self
.__isDeadWxObject
(aTopic
[1]) 
 281     def __haveMessageAdapter(self
, eventHandler
, topicPattern
): 
 283         Return True if there's already a message adapter 
 287             return self
.messageAdapterDict
[topicPattern
].has_key(eventHandler
) 
 292     def _determineWindow(self
, aComponent
): 
 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. 
 301         if isinstance(aComponent
, wx
.MenuItem
): 
 302             return aComponent
.GetMenu() 
 308 #--------------------------------------------------------------------------- 
 309 # From here down is implementaion and support classes, although you may 
 310 # find some of them useful in other contexts. 
 311 #--------------------------------------------------------------------------- 
 314 class EventMacroInfo
: 
 316     A class that provides information about event macros. 
 319         self
.lookupTable 
= {} 
 322     def getEventTypes(self
, eventMacro
): 
 324         Return the list of event types that the given 
 325         macro corresponds to. 
 328             return self
.lookupTable
[eventMacro
] 
 332                 eventMacro(win
, None, None) 
 333             except (TypeError, AssertionError): 
 334                 eventMacro(win
, None) 
 335             self
.lookupTable
[eventMacro
] = win
.eventTypes
 
 336             return win
.eventTypes
 
 339     def eventIsA(self
, event
, macroList
): 
 341         Return True if the event is one of the given 
 344         eventType 
= event
.GetEventType() 
 345         for macro 
in macroList
: 
 346             if eventType 
in self
.getEventTypes(macro
): 
 351     def macroIsA(self
, macro
, macroList
): 
 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. 
 358         if callable(macroList
): 
 359             macroList 
= (macroList
,) 
 360         testList  
= self
.getEventTypes(macro
) 
 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
: 
 371     def isMultiEvent(self
, macro
): 
 373         Return True if the given macro actually causes 
 374         multiple events to be registered. 
 376         return len(self
.getEventTypes(macro
)) > 1 
 379 #--------------------------------------------------------------------------- 
 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 
 392     def Connect(self
, id1
, id2
, eventType
, handlerFunction
): 
 393         self
.eventTypes
.append(eventType
) 
 396 #--------------------------------------------------------------------------- 
 400     A class that adapts incoming wxWindows events to 
 401     Publish/Subscribe messages. 
 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. 
 408     def __init__(self
, func
, win
, id): 
 410         Instantiate a new adapter. Pre-compute my Publish/Subscribe 
 411         topic, which is constant, and register with wxWindows. 
 413         self
.publisher 
= pubsub
.Publisher() 
 414         self
.topic     
= ((func
, win
, id),) 
 417         self
.eventType 
= _macroInfo
.getEventTypes(func
)[0] 
 419         # Register myself with the wxWindows event system 
 421             func(win
, id, self
.handleEvent
) 
 423         except (TypeError, AssertionError): 
 424             func(win
, self
.handleEvent
) 
 428     def disconnect(self
): 
 429         if self
.callStyle 
== 3: 
 430             return self
.win
.Disconnect(self
.id, -1, self
.eventType
) 
 432             return self
.win
.Disconnect(-1, -1, self
.eventType
) 
 435     def handleEvent(self
, event
): 
 437         In response to a wxWindows event, send a PS message 
 439         self
.publisher
.sendMessage(topic
=self
.topic
, data
=event
) 
 444             if not self
.disconnect(): 
 445                 print 'disconnect failed' 
 446         except wx
.PyDeadObjectError
: 
 447             print 'disconnect failed: dead object'              ##???? 
 450 #--------------------------------------------------------------------------- 
 452 class MessageAdapter
: 
 454     A class that adapts incoming Publish/Subscribe messages 
 455     to wxWindows event calls. 
 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 
 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. 
 467     def __init__(self
, eventHandler
, topicPattern
): 
 469         Instantiate a new MessageAdapter that send wxEvents to the 
 472         self
.eventHandler 
= eventHandler
 
 473         pubsub
.Publisher().subscribe(listener
=self
.deliverEvent
, topic
=(topicPattern
,)) 
 475     def deliverEvent(self
, message
): 
 476         event 
= message
.data        
# Extract the wxEvent 
 477         self
.eventHandler(event
)    # Perform the call as wxWindows would 
 480         pubsub
.Publisher().unsubscribe(listener
=self
.deliverEvent
) 
 483 #--------------------------------------------------------------------------- 
 486 _macroInfo    
= EventMacroInfo() 
 488 # For now a singleton is not enforced.  Should it be or can we trust 
 490 eventManager  
= EventManager() 
 493 #--------------------------------------------------------------------------- 
 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
) 
 507     # Demonstrate 1) register/deregister, 2) Multiple listeners receiving 
 508     # one event, and 3) Multiple events going to one listener. 
 511     def printEvent(event
): 
 512         print 'Name:',event
.GetClassName(),'Timestamp',event
.GetTimestamp() 
 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
) 
 521             print '\nDisabling mouse events...' 
 522             eventManager
.DeregisterWindow(frame
) 
 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
) 
 529     frame
.CenterOnScreen()