git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@27848 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775
		
			
				
	
	
		
			532 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			532 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #---------------------------------------------------------------------------
 | |
| # Name:        wxPython.lib.evtmgr
 | |
| # Purpose:     An easier, more "Pythonic" and more OO method of registering
 | |
| #              handlers for wxWindows events using the Publish/Subscribe
 | |
| #              pattern.
 | |
| #
 | |
| # Author:      Robb Shecter and Robin Dunn
 | |
| #
 | |
| # Created:     12-December-2002
 | |
| # RCS-ID:      $Id$
 | |
| # Copyright:   (c) 2003 by db-X Corporation
 | |
| # Licence:     wxWindows license
 | |
| #---------------------------------------------------------------------------
 | |
| # 12/02/2003 - Jeff Grimmett (grimmtooth@softhome.net)
 | |
| #
 | |
| # o Updated for 2.5 compatability.
 | |
| #
 | |
| 
 | |
| """
 | |
| A module that allows multiple handlers to respond to single wxWindows
 | |
| events.  This allows true NxN Observer/Observable connections: One
 | |
| event can be received by multiple handlers, and one handler can
 | |
| receive multiple events.
 | |
| 
 | |
| There are two ways to register event handlers.  The first way is
 | |
| similar to standard wxPython handler registration:
 | |
| 
 | |
|     from wxPython.lib.evtmgr import eventManager
 | |
|     eventManager.Register(handleEvents, EVT_BUTTON, win=frame, id=101)
 | |
| 
 | |
| There's also a new object-oriented way to register for events.  This
 | |
| invocation is equivalent to the one above, but does not require the
 | |
| programmer to declare or track control ids or parent containers:
 | |
| 
 | |
|     eventManager.Register(handleEvents, EVT_BUTTON, myButton)
 | |
| 
 | |
| This module is Python 2.1+ compatible.
 | |
| 
 | |
| """
 | |
| import  wx
 | |
| import  pubsub # publish / subscribe library
 | |
| 
 | |
| #---------------------------------------------------------------------------
 | |
| 
 | |
| 
 | |
| class EventManager:
 | |
|     """
 | |
|     This is the main class in the module, and is the only class that
 | |
|     the application programmer needs to use.  There is a pre-created
 | |
|     instance of this class called 'eventManager'.  It should not be
 | |
|     necessary to create other instances.
 | |
|     """
 | |
|     def __init__(self):
 | |
|         self.eventAdapterDict    = {}
 | |
|         self.messageAdapterDict  = {}
 | |
|         self.windowTopicLookup   = {}
 | |
|         self.listenerTopicLookup = {}
 | |
|         self.__publisher         = pubsub.Publisher()
 | |
|         self.EMPTY_LIST          = []
 | |
| 
 | |
| 
 | |
|     def Register(self, listener, event, source=None, win=None, id=None):
 | |
|         """
 | |
|         Registers a listener function (or any callable object) to
 | |
|         receive events of type event coming from the source window.
 | |
|         For example::
 | |
|             eventManager.Register(self.OnButton, EVT_BUTTON, theButton)
 | |
| 
 | |
|         Alternatively, the specific window where the event is
 | |
|         delivered, and/or the ID of the event source can be specified.
 | |
|         For example::
 | |
|             eventManager.Register(self.OnButton, EVT_BUTTON, win=self, id=ID_BUTTON)
 | |
|             
 | |
|         or::
 | |
|             eventManager.Register(self.OnButton, EVT_BUTTON, theButton, self)
 | |
|             
 | |
|         """
 | |
| 
 | |
|         # 1. Check if the 'event' is actually one of the multi-
 | |
|         #    event macros.
 | |
|         if _macroInfo.isMultiEvent(event):
 | |
|             raise 'Cannot register the macro, '+`event`+'.  Register instead the individual events.'
 | |
| 
 | |
|         # Support a more OO API.  This allows the GUI widget itself to
 | |
|         # be specified, and the id to be retrieved from the system,
 | |
|         # instead of kept track of explicitly by the programmer.
 | |
|         # (Being used to doing GUI work with Java, this seems to me to be
 | |
|         # the natural way of doing things.)
 | |
|         if source is not None:
 | |
|             id  = source.GetId()
 | |
|             
 | |
|         if win is None:
 | |
|             # Some widgets do not function as their own windows.
 | |
|             win = self._determineWindow(source)
 | |
|             
 | |
|         topic = (event, win, id)
 | |
| 
 | |
|         #  Create an adapter from the PS system back to wxEvents, and
 | |
|         #  possibly one from wxEvents:
 | |
|         if not self.__haveMessageAdapter(listener, topic):
 | |
|             messageAdapter = MessageAdapter(eventHandler=listener, topicPattern=topic)
 | |
|             try:
 | |
|                 self.messageAdapterDict[topic][listener] = messageAdapter
 | |
|             except KeyError:
 | |
|                 self.messageAdapterDict[topic] = {}
 | |
|                 self.messageAdapterDict[topic][listener] = messageAdapter
 | |
| 
 | |
|             if not self.eventAdapterDict.has_key(topic):
 | |
|                 self.eventAdapterDict[topic] = EventAdapter(event, win, id)
 | |
|         else:
 | |
|             # Throwing away a duplicate request
 | |
|             pass
 | |
| 
 | |
|         # For time efficiency when deregistering by window:
 | |
|         try:
 | |
|             self.windowTopicLookup[win].append(topic)
 | |
|         except KeyError:
 | |
|             self.windowTopicLookup[win] = []
 | |
|             self.windowTopicLookup[win].append(topic)
 | |
| 
 | |
|         # For time efficiency when deregistering by listener:
 | |
|         try:
 | |
|             self.listenerTopicLookup[listener].append(topic)
 | |
|         except KeyError:
 | |
|             self.listenerTopicLookup[listener] = []
 | |
|             self.listenerTopicLookup[listener].append(topic)
 | |
| 
 | |
|         # See if the source understands the listeningFor protocol.
 | |
|         # This is a bit of a test I'm working on - it allows classes
 | |
|         # to know when their events are being listened to.  I use
 | |
|         # it to enable chaining events from contained windows only
 | |
|         # when needed.
 | |
|         if source is not None:
 | |
|             try:
 | |
|                 # Let the source know that we're listening  for this
 | |
|                 # event.
 | |
|                 source.listeningFor(event)
 | |
|             except AttributeError:
 | |
|                 pass
 | |
| 
 | |
|     # Some aliases for Register, just for kicks
 | |
|     Bind = Register
 | |
|     Subscribe = Register
 | |
| 
 | |
| 
 | |
|     def DeregisterWindow(self, win):
 | |
|         """
 | |
|         Deregister all events coming from the given window.
 | |
|         """
 | |
|         win    = self._determineWindow(win)
 | |
|         topics = self.__getTopics(win)
 | |
| 
 | |
|         if topics:
 | |
|             for aTopic in topics:
 | |
|                 self.__deregisterTopic(aTopic)
 | |
| 
 | |
|             del self.windowTopicLookup[win]
 | |
| 
 | |
| 
 | |
|     def DeregisterListener(self, listener):
 | |
|         """
 | |
|         Deregister all event notifications for the given listener.
 | |
|         """
 | |
|         try:
 | |
|             topicList = self.listenerTopicLookup[listener]
 | |
|         except KeyError:
 | |
|             return
 | |
| 
 | |
|         for topic in topicList:
 | |
|             topicDict = self.messageAdapterDict[topic]
 | |
| 
 | |
|             if topicDict.has_key(listener):
 | |
|                 topicDict[listener].Destroy()
 | |
|                 del topicDict[listener]
 | |
| 
 | |
|                 if len(topicDict) == 0:
 | |
|                     self.eventAdapterDict[topic].Destroy()
 | |
|                     del self.eventAdapterDict[topic]
 | |
|                     del self.messageAdapterDict[topic]
 | |
| 
 | |
|         del self.listenerTopicLookup[listener]
 | |
| 
 | |
| 
 | |
|     def GetStats(self):
 | |
|         """
 | |
|         Return a dictionary with data about my state.
 | |
|         """
 | |
|         stats = {}
 | |
|         stats['Adapters: Message'] = reduce(lambda x,y: x+y, [0] + map(len, self.messageAdapterDict.values()))
 | |
|         stats['Adapters: Event']   = len(self.eventAdapterDict)
 | |
|         stats['Topics: Total']     = len(self.__getTopics())
 | |
|         stats['Topics: Dead']      = len(self.GetDeadTopics())
 | |
|         return stats
 | |
| 
 | |
| 
 | |
|     def DeregisterDeadTopics(self):
 | |
|         """
 | |
|         Deregister any entries relating to dead
 | |
|         wxPython objects.  Not sure if this is an
 | |
|         important issue; 1) My app code always de-registers
 | |
|         listeners it doesn't need.  2) I don't think
 | |
|         that lingering references to these dead objects
 | |
|         is a problem.
 | |
|         """
 | |
|         for topic in self.GetDeadTopics():
 | |
|             self.__deregisterTopic(topic)
 | |
| 
 | |
| 
 | |
|     def GetDeadTopics(self):
 | |
|         """
 | |
|         Return a list of topics relating to dead wxPython
 | |
|         objects.
 | |
|         """
 | |
|         return filter(self.__isDeadTopic, self.__getTopics())
 | |
| 
 | |
| 
 | |
|     def __winString(self, aWin):
 | |
|         """
 | |
|         A string rep of a window for debugging
 | |
|         """
 | |
|         try:
 | |
|             name = aWin.GetClassName()
 | |
|             i    = id(aWin)
 | |
|             return '%s #%d' % (name, i)
 | |
|         except wx.PyDeadObjectError:
 | |
|             return '(dead wx.Object)'
 | |
| 
 | |
| 
 | |
|     def __topicString(self, aTopic):
 | |
|         """
 | |
|         A string rep of a topic for debugging
 | |
|         """
 | |
|         return '[%-26s %s]' % (aTopic[0].__name__, self.winString(aTopic[1]))
 | |
| 
 | |
| 
 | |
|     def __listenerString(self, aListener):
 | |
|         """
 | |
|         A string rep of a listener for debugging
 | |
|         """
 | |
|         try:
 | |
|             return aListener.im_class.__name__ + '.' + aListener.__name__
 | |
|         except:
 | |
|             return 'Function ' + aListener.__name__
 | |
| 
 | |
| 
 | |
|     def __deregisterTopic(self, aTopic):
 | |
|         try:
 | |
|             messageAdapterList = self.messageAdapterDict[aTopic].values()
 | |
|         except KeyError:
 | |
|             # This topic isn't valid.  Probably because it was deleted
 | |
|             # by listener.
 | |
|             return
 | |
| 
 | |
|         for messageAdapter in messageAdapterList:
 | |
|             messageAdapter.Destroy()
 | |
| 
 | |
|         self.eventAdapterDict[aTopic].Destroy()
 | |
|         del self.messageAdapterDict[aTopic]
 | |
|         del self.eventAdapterDict[aTopic]
 | |
| 
 | |
| 
 | |
|     def __getTopics(self, win=None):
 | |
|         if win is None:
 | |
|             return self.messageAdapterDict.keys()
 | |
| 
 | |
|         if win is not None:
 | |
|             try:
 | |
|                 return self.windowTopicLookup[win]
 | |
|             except KeyError:
 | |
|                 return self.EMPTY_LIST
 | |
| 
 | |
| 
 | |
|     def __isDeadWxObject(self, anObject):
 | |
|         return isinstance(anObject, wx._wxPyDeadObject)
 | |
| 
 | |
| 
 | |
|     def __isDeadTopic(self, aTopic):
 | |
|         return self.__isDeadWxObject(aTopic[1])
 | |
| 
 | |
| 
 | |
|     def __haveMessageAdapter(self, eventHandler, topicPattern):
 | |
|         """
 | |
|         Return True if there's already a message adapter
 | |
|         with these specs.
 | |
|         """
 | |
|         try:
 | |
|             return self.messageAdapterDict[topicPattern].has_key(eventHandler)
 | |
|         except KeyError:
 | |
|             return 0
 | |
| 
 | |
| 
 | |
|     def _determineWindow(self, aComponent):
 | |
|         """
 | |
|         Return the window that corresponds to this component.
 | |
|         A window is something that supports the Connect protocol.
 | |
|         Most things registered with the event manager are a window,
 | |
|         but there are apparently some exceptions.  If more are
 | |
|         discovered, the implementation can be changed to a dictionary
 | |
|         lookup along the lines of class : function-to-get-window.
 | |
|         """
 | |
|         if isinstance(aComponent, wx.MenuItem):
 | |
|             return aComponent.GetMenu()
 | |
|         else:
 | |
|             return aComponent
 | |
| 
 | |
| 
 | |
| 
 | |
| #---------------------------------------------------------------------------
 | |
| # From here down is implementaion and support classes, although you may
 | |
| # find some of them useful in other contexts.
 | |
| #---------------------------------------------------------------------------
 | |
| 
 | |
| 
 | |
| class EventMacroInfo:
 | |
|     """
 | |
|     A class that provides information about event macros.
 | |
|     """
 | |
|     def __init__(self):
 | |
|         self.lookupTable = {}
 | |
| 
 | |
| 
 | |
|     def getEventTypes(self, eventMacro):
 | |
|         """
 | |
|         Return the list of event types that the given
 | |
|         macro corresponds to.
 | |
|         """
 | |
|         try:
 | |
|             return self.lookupTable[eventMacro]
 | |
|         except KeyError:
 | |
|             win = FakeWindow()
 | |
|             try:
 | |
|                 eventMacro(win, None, None)
 | |
|             except (TypeError, AssertionError):
 | |
|                 eventMacro(win, None)
 | |
|             self.lookupTable[eventMacro] = win.eventTypes
 | |
|             return win.eventTypes
 | |
| 
 | |
| 
 | |
|     def eventIsA(self, event, macroList):
 | |
|         """
 | |
|         Return True if the event is one of the given
 | |
|         macros.
 | |
|         """
 | |
|         eventType = event.GetEventType()
 | |
|         for macro in macroList:
 | |
|             if eventType in self.getEventTypes(macro):
 | |
|                 return 1
 | |
|         return 0
 | |
| 
 | |
| 
 | |
|     def macroIsA(self, macro, macroList):
 | |
|         """
 | |
|         Return True if the macro is in the macroList.
 | |
|         The added value of this method is that it takes
 | |
|         multi-events into account.  The macroList parameter
 | |
|         will be coerced into a sequence if needed.
 | |
|         """
 | |
|         if callable(macroList):
 | |
|             macroList = (macroList,)
 | |
|         testList  = self.getEventTypes(macro)
 | |
|         eventList = []
 | |
|         for m in macroList:
 | |
|             eventList.extend(self.getEventTypes(m))
 | |
|         # Return True if every element in testList is in eventList
 | |
|         for element in testList:
 | |
|             if element not in eventList:
 | |
|                 return 0
 | |
|         return 1
 | |
| 
 | |
| 
 | |
|     def isMultiEvent(self, macro):
 | |
|         """
 | |
|         Return True if the given macro actually causes
 | |
|         multiple events to be registered.
 | |
|         """
 | |
|         return len(self.getEventTypes(macro)) > 1
 | |
| 
 | |
| 
 | |
| #---------------------------------------------------------------------------
 | |
| 
 | |
| class FakeWindow:
 | |
|     """
 | |
|     Used internally by the EventMacroInfo class.  The FakeWindow is
 | |
|     the most important component of the macro-info utility: it
 | |
|     implements the Connect() protocol of wxWindow, but instead of
 | |
|     registering for events, it keeps track of what parameters were
 | |
|     passed to it.
 | |
|     """
 | |
|     def __init__(self):
 | |
|         self.eventTypes = []
 | |
| 
 | |
|     def Connect(self, id1, id2, eventType, handlerFunction):
 | |
|         self.eventTypes.append(eventType)
 | |
| 
 | |
| 
 | |
| #---------------------------------------------------------------------------
 | |
| 
 | |
| class EventAdapter:
 | |
|     """
 | |
|     A class that adapts incoming wxWindows events to
 | |
|     Publish/Subscribe messages.
 | |
| 
 | |
|     In other words, this is the object that's seen by the
 | |
|     wxWindows system.  Only one of these registers for any
 | |
|     particular wxWindows event.  It then relays it into the
 | |
|     PS system, which lets many listeners respond.
 | |
|     """
 | |
|     def __init__(self, func, win, id):
 | |
|         """
 | |
|         Instantiate a new adapter. Pre-compute my Publish/Subscribe
 | |
|         topic, which is constant, and register with wxWindows.
 | |
|         """
 | |
|         self.publisher = pubsub.Publisher()
 | |
|         self.topic     = ((func, win, id),)
 | |
|         self.id        = id
 | |
|         self.win       = win
 | |
|         self.eventType = _macroInfo.getEventTypes(func)[0]
 | |
| 
 | |
|         # Register myself with the wxWindows event system
 | |
|         try:
 | |
|             func(win, id, self.handleEvent)
 | |
|             self.callStyle = 3
 | |
|         except (TypeError, AssertionError):
 | |
|             func(win, self.handleEvent)
 | |
|             self.callStyle = 2
 | |
| 
 | |
| 
 | |
|     def disconnect(self):
 | |
|         if self.callStyle == 3:
 | |
|             return self.win.Disconnect(self.id, -1, self.eventType)
 | |
|         else:
 | |
|             return self.win.Disconnect(-1, -1, self.eventType)
 | |
| 
 | |
| 
 | |
|     def handleEvent(self, event):
 | |
|         """
 | |
|         In response to a wxWindows event, send a PS message
 | |
|         """
 | |
|         self.publisher.sendMessage(topic=self.topic, data=event)
 | |
| 
 | |
| 
 | |
|     def Destroy(self):
 | |
|         try:
 | |
|             if not self.disconnect():
 | |
|                 print 'disconnect failed'
 | |
|         except wx.PyDeadObjectError:
 | |
|             print 'disconnect failed: dead object'              ##????
 | |
| 
 | |
| 
 | |
| #---------------------------------------------------------------------------
 | |
| 
 | |
| class MessageAdapter:
 | |
|     """
 | |
|     A class that adapts incoming Publish/Subscribe messages
 | |
|     to wxWindows event calls.
 | |
| 
 | |
|     This class works opposite the EventAdapter, and
 | |
|     retrieves the information an EventAdapter has sent in a message.
 | |
|     Strictly speaking, this class is not required: Event listeners
 | |
|     could pull the original wxEvent object out of the PS Message
 | |
|     themselves.
 | |
| 
 | |
|     However, by pairing an instance of this class with each wxEvent
 | |
|     handler, the handlers can use the standard API: they receive an
 | |
|     event as a parameter.
 | |
|     """
 | |
|     def __init__(self, eventHandler, topicPattern):
 | |
|         """
 | |
|         Instantiate a new MessageAdapter that send wxEvents to the
 | |
|         given eventHandler.
 | |
|         """
 | |
|         self.eventHandler = eventHandler
 | |
|         pubsub.Publisher().subscribe(listener=self.deliverEvent, topic=(topicPattern,))
 | |
| 
 | |
|     def deliverEvent(self, message):
 | |
|         event = message.data        # Extract the wxEvent
 | |
|         self.eventHandler(event)    # Perform the call as wxWindows would
 | |
| 
 | |
|     def Destroy(self):
 | |
|         pubsub.Publisher().unsubscribe(listener=self.deliverEvent)
 | |
| 
 | |
| 
 | |
| #---------------------------------------------------------------------------
 | |
| # Create globals
 | |
| 
 | |
| _macroInfo    = EventMacroInfo()
 | |
| 
 | |
| # For now a singleton is not enforced.  Should it be or can we trust
 | |
| # the programmers?
 | |
| eventManager  = EventManager()
 | |
| 
 | |
| 
 | |
| #---------------------------------------------------------------------------
 | |
| # simple test code
 | |
| 
 | |
| 
 | |
| if __name__ == '__main__':
 | |
|     app    = wx.PySimpleApp()
 | |
|     frame  = wx.Frame(None, -1, 'Event Test', size=(300,300))
 | |
|     button = wx.ToggleButton(frame, -1, 'Listen for Mouse Events')
 | |
|     sizer  = wx.BoxSizer(wx.HORIZONTAL)
 | |
|     sizer.Add(button, 0, 0 | wx.ALL, 10)
 | |
|     frame.SetAutoLayout(1)
 | |
|     frame.SetSizer(sizer)
 | |
| 
 | |
|     #
 | |
|     # Demonstrate 1) register/deregister, 2) Multiple listeners receiving
 | |
|     # one event, and 3) Multiple events going to one listener.
 | |
|     #
 | |
| 
 | |
|     def printEvent(event):
 | |
|         print 'Name:',event.GetClassName(),'Timestamp',event.GetTimestamp()
 | |
| 
 | |
|     def enableFrameEvents(event):
 | |
|         # Turn the output of mouse events on and off
 | |
|         if event.IsChecked():
 | |
|             print '\nEnabling mouse events...'
 | |
|             eventManager.Register(printEvent, wx.EVT_MOTION,    frame)
 | |
|             eventManager.Register(printEvent, wx.EVT_LEFT_DOWN, frame)
 | |
|         else:
 | |
|             print '\nDisabling mouse events...'
 | |
|             eventManager.DeregisterWindow(frame)
 | |
| 
 | |
|     # Send togglebutton events to both the on/off code as well
 | |
|     # as the function that prints to stdout.
 | |
|     eventManager.Register(printEvent,        wx.EVT_TOGGLEBUTTON, button)
 | |
|     eventManager.Register(enableFrameEvents, wx.EVT_TOGGLEBUTTON, button)
 | |
| 
 | |
|     frame.CenterOnScreen()
 | |
|     frame.Show(1)
 | |
|     app.MainLoop()
 |