]>
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
._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()