X-Git-Url: https://git.saurik.com/wxWidgets.git/blobdiff_plain/1fded56b375bf7a4687af1cdb182899614c1b2a8..35d8bffe0132c6456678045a8ada4ca104bbc6f2:/wxPython/wx/lib/evtmgr.py diff --git a/wxPython/wx/lib/evtmgr.py b/wxPython/wx/lib/evtmgr.py index c010bf13f3..439ccc76f3 100644 --- a/wxPython/wx/lib/evtmgr.py +++ b/wxPython/wx/lib/evtmgr.py @@ -1,11 +1,534 @@ +#--------------------------------------------------------------------------- +# 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. +# -"""Renamer stub: provides a way to drop the wx prefix from wxPython objects.""" +""" +A module that allows multiple handlers to respond to single wxWidgets +events. This allows true NxN Observer/Observable connections: One +event can be received by multiple handlers, and one handler can +receive multiple events. -__cvsid__ = "$Id$" -__revision__ = "$Revision$"[11:-2] +There are two ways to register event handlers. The first way is +similar to standard wxPython handler registration:: -from wx import _rename -from wxPython.lib import evtmgr -_rename(globals(), evtmgr.__dict__, modulename='lib.evtmgr') -del evtmgr -del _rename + from wx.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._core._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()