]> git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/evtmgr.py
Patch from Andrea that fixes the following problems/issues:
[wxWidgets.git] / 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
5 # pattern.
6 #
7 # Author: Robb Shecter and Robin Dunn
8 #
9 # Created: 12-December-2002
10 # RCS-ID: $Id$
11 # Copyright: (c) 2003 by db-X Corporation
12 # Licence: wxWindows license
13 #---------------------------------------------------------------------------
14 # 12/02/2003 - Jeff Grimmett (grimmtooth@softhome.net)
15 #
16 # o Updated for 2.5 compatability.
17 #
18
19 """
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.
24
25 There are two ways to register event handlers. The first way is
26 similar to standard wxPython handler registration::
27
28 from wx.lib.evtmgr import eventManager
29 eventManager.Register(handleEvents, EVT_BUTTON, win=frame, id=101)
30
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::
34
35 eventManager.Register(handleEvents, EVT_BUTTON, myButton)
36
37 This module is Python 2.1+ compatible.
38
39 """
40 import wx
41 import pubsub # publish / subscribe library
42
43 #---------------------------------------------------------------------------
44
45
46 class EventManager:
47 """
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.
52 """
53 def __init__(self):
54 self.eventAdapterDict = {}
55 self.messageAdapterDict = {}
56 self.windowTopicLookup = {}
57 self.listenerTopicLookup = {}
58 self.__publisher = pubsub.Publisher()
59 self.EMPTY_LIST = []
60
61
62 def Register(self, listener, event, source=None, win=None, id=None):
63 """
64 Registers a listener function (or any callable object) to
65 receive events of type event coming from the source window.
66 For example::
67
68 eventManager.Register(self.OnButton, EVT_BUTTON, theButton)
69
70 Alternatively, the specific window where the event is
71 delivered, and/or the ID of the event source can be specified.
72 For example::
73
74 eventManager.Register(self.OnButton, EVT_BUTTON, win=self, id=ID_BUTTON)
75
76 or::
77
78 eventManager.Register(self.OnButton, EVT_BUTTON, theButton, self)
79
80 """
81
82 # 1. Check if the 'event' is actually one of the multi-
83 # event macros.
84 if _macroInfo.isMultiEvent(event):
85 raise 'Cannot register the macro, '+`event`+'. Register instead the individual events.'
86
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:
93 id = source.GetId()
94
95 if win is None:
96 # Some widgets do not function as their own windows.
97 win = self._determineWindow(source)
98
99 topic = (event, win, id)
100
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)
105 try:
106 self.messageAdapterDict[topic][listener] = messageAdapter
107 except KeyError:
108 self.messageAdapterDict[topic] = {}
109 self.messageAdapterDict[topic][listener] = messageAdapter
110
111 if not self.eventAdapterDict.has_key(topic):
112 self.eventAdapterDict[topic] = EventAdapter(event, win, id)
113 else:
114 # Throwing away a duplicate request
115 pass
116
117 # For time efficiency when deregistering by window:
118 try:
119 self.windowTopicLookup[win].append(topic)
120 except KeyError:
121 self.windowTopicLookup[win] = []
122 self.windowTopicLookup[win].append(topic)
123
124 # For time efficiency when deregistering by listener:
125 try:
126 self.listenerTopicLookup[listener].append(topic)
127 except KeyError:
128 self.listenerTopicLookup[listener] = []
129 self.listenerTopicLookup[listener].append(topic)
130
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
135 # when needed.
136 if source is not None:
137 try:
138 # Let the source know that we're listening for this
139 # event.
140 source.listeningFor(event)
141 except AttributeError:
142 pass
143
144 # Some aliases for Register, just for kicks
145 Bind = Register
146 Subscribe = Register
147
148
149 def DeregisterWindow(self, win):
150 """
151 Deregister all events coming from the given window.
152 """
153 win = self._determineWindow(win)
154 topics = self.__getTopics(win)
155
156 if topics:
157 for aTopic in topics:
158 self.__deregisterTopic(aTopic)
159
160 del self.windowTopicLookup[win]
161
162
163 def DeregisterListener(self, listener):
164 """
165 Deregister all event notifications for the given listener.
166 """
167 try:
168 topicList = self.listenerTopicLookup[listener]
169 except KeyError:
170 return
171
172 for topic in topicList:
173 topicDict = self.messageAdapterDict[topic]
174
175 if topicDict.has_key(listener):
176 topicDict[listener].Destroy()
177 del topicDict[listener]
178
179 if len(topicDict) == 0:
180 self.eventAdapterDict[topic].Destroy()
181 del self.eventAdapterDict[topic]
182 del self.messageAdapterDict[topic]
183
184 del self.listenerTopicLookup[listener]
185
186
187 def GetStats(self):
188 """
189 Return a dictionary with data about my state.
190 """
191 stats = {}
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())
196 return stats
197
198
199 def DeregisterDeadTopics(self):
200 """
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
206 is a problem.
207 """
208 for topic in self.GetDeadTopics():
209 self.__deregisterTopic(topic)
210
211
212 def GetDeadTopics(self):
213 """
214 Return a list of topics relating to dead wxPython
215 objects.
216 """
217 return filter(self.__isDeadTopic, self.__getTopics())
218
219
220 def __winString(self, aWin):
221 """
222 A string rep of a window for debugging
223 """
224 try:
225 name = aWin.GetClassName()
226 i = id(aWin)
227 return '%s #%d' % (name, i)
228 except wx.PyDeadObjectError:
229 return '(dead wx.Object)'
230
231
232 def __topicString(self, aTopic):
233 """
234 A string rep of a topic for debugging
235 """
236 return '[%-26s %s]' % (aTopic[0].__name__, self.winString(aTopic[1]))
237
238
239 def __listenerString(self, aListener):
240 """
241 A string rep of a listener for debugging
242 """
243 try:
244 return aListener.im_class.__name__ + '.' + aListener.__name__
245 except:
246 return 'Function ' + aListener.__name__
247
248
249 def __deregisterTopic(self, aTopic):
250 try:
251 messageAdapterList = self.messageAdapterDict[aTopic].values()
252 except KeyError:
253 # This topic isn't valid. Probably because it was deleted
254 # by listener.
255 return
256
257 for messageAdapter in messageAdapterList:
258 messageAdapter.Destroy()
259
260 self.eventAdapterDict[aTopic].Destroy()
261 del self.messageAdapterDict[aTopic]
262 del self.eventAdapterDict[aTopic]
263
264
265 def __getTopics(self, win=None):
266 if win is None:
267 return self.messageAdapterDict.keys()
268
269 if win is not None:
270 try:
271 return self.windowTopicLookup[win]
272 except KeyError:
273 return self.EMPTY_LIST
274
275
276 def __isDeadWxObject(self, anObject):
277 return isinstance(anObject, wx._core._wxPyDeadObject)
278
279
280 def __isDeadTopic(self, aTopic):
281 return self.__isDeadWxObject(aTopic[1])
282
283
284 def __haveMessageAdapter(self, eventHandler, topicPattern):
285 """
286 Return True if there's already a message adapter
287 with these specs.
288 """
289 try:
290 return self.messageAdapterDict[topicPattern].has_key(eventHandler)
291 except KeyError:
292 return 0
293
294
295 def _determineWindow(self, aComponent):
296 """
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.
303 """
304 if isinstance(aComponent, wx.MenuItem):
305 return aComponent.GetMenu()
306 else:
307 return aComponent
308
309
310
311 #---------------------------------------------------------------------------
312 # From here down is implementaion and support classes, although you may
313 # find some of them useful in other contexts.
314 #---------------------------------------------------------------------------
315
316
317 class EventMacroInfo:
318 """
319 A class that provides information about event macros.
320 """
321 def __init__(self):
322 self.lookupTable = {}
323
324
325 def getEventTypes(self, eventMacro):
326 """
327 Return the list of event types that the given
328 macro corresponds to.
329 """
330 try:
331 return self.lookupTable[eventMacro]
332 except KeyError:
333 win = FakeWindow()
334 try:
335 eventMacro(win, None, None)
336 except (TypeError, AssertionError):
337 eventMacro(win, None)
338 self.lookupTable[eventMacro] = win.eventTypes
339 return win.eventTypes
340
341
342 def eventIsA(self, event, macroList):
343 """
344 Return True if the event is one of the given
345 macros.
346 """
347 eventType = event.GetEventType()
348 for macro in macroList:
349 if eventType in self.getEventTypes(macro):
350 return 1
351 return 0
352
353
354 def macroIsA(self, macro, macroList):
355 """
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.
360 """
361 if callable(macroList):
362 macroList = (macroList,)
363 testList = self.getEventTypes(macro)
364 eventList = []
365 for m in macroList:
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:
370 return 0
371 return 1
372
373
374 def isMultiEvent(self, macro):
375 """
376 Return True if the given macro actually causes
377 multiple events to be registered.
378 """
379 return len(self.getEventTypes(macro)) > 1
380
381
382 #---------------------------------------------------------------------------
383
384 class FakeWindow:
385 """
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
390 passed to it.
391 """
392 def __init__(self):
393 self.eventTypes = []
394
395 def Connect(self, id1, id2, eventType, handlerFunction):
396 self.eventTypes.append(eventType)
397
398
399 #---------------------------------------------------------------------------
400
401 class EventAdapter:
402 """
403 A class that adapts incoming wxWindows events to
404 Publish/Subscribe messages.
405
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.
410 """
411 def __init__(self, func, win, id):
412 """
413 Instantiate a new adapter. Pre-compute my Publish/Subscribe
414 topic, which is constant, and register with wxWindows.
415 """
416 self.publisher = pubsub.Publisher()
417 self.topic = ((func, win, id),)
418 self.id = id
419 self.win = win
420 self.eventType = _macroInfo.getEventTypes(func)[0]
421
422 # Register myself with the wxWindows event system
423 try:
424 func(win, id, self.handleEvent)
425 self.callStyle = 3
426 except (TypeError, AssertionError):
427 func(win, self.handleEvent)
428 self.callStyle = 2
429
430
431 def disconnect(self):
432 if self.callStyle == 3:
433 return self.win.Disconnect(self.id, -1, self.eventType)
434 else:
435 return self.win.Disconnect(-1, -1, self.eventType)
436
437
438 def handleEvent(self, event):
439 """
440 In response to a wxWindows event, send a PS message
441 """
442 self.publisher.sendMessage(topic=self.topic, data=event)
443
444
445 def Destroy(self):
446 try:
447 if not self.disconnect():
448 print 'disconnect failed'
449 except wx.PyDeadObjectError:
450 print 'disconnect failed: dead object' ##????
451
452
453 #---------------------------------------------------------------------------
454
455 class MessageAdapter:
456 """
457 A class that adapts incoming Publish/Subscribe messages
458 to wxWindows event calls.
459
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
464 themselves.
465
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.
469 """
470 def __init__(self, eventHandler, topicPattern):
471 """
472 Instantiate a new MessageAdapter that send wxEvents to the
473 given eventHandler.
474 """
475 self.eventHandler = eventHandler
476 pubsub.Publisher().subscribe(listener=self.deliverEvent, topic=(topicPattern,))
477
478 def deliverEvent(self, message):
479 event = message.data # Extract the wxEvent
480 self.eventHandler(event) # Perform the call as wxWindows would
481
482 def Destroy(self):
483 pubsub.Publisher().unsubscribe(listener=self.deliverEvent)
484
485
486 #---------------------------------------------------------------------------
487 # Create globals
488
489 _macroInfo = EventMacroInfo()
490
491 # For now a singleton is not enforced. Should it be or can we trust
492 # the programmers?
493 eventManager = EventManager()
494
495
496 #---------------------------------------------------------------------------
497 # simple test code
498
499
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)
508
509 #
510 # Demonstrate 1) register/deregister, 2) Multiple listeners receiving
511 # one event, and 3) Multiple events going to one listener.
512 #
513
514 def printEvent(event):
515 print 'Name:',event.GetClassName(),'Timestamp',event.GetTimestamp()
516
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)
523 else:
524 print '\nDisabling mouse events...'
525 eventManager.DeregisterWindow(frame)
526
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)
531
532 frame.CenterOnScreen()
533 frame.Show(1)
534 app.MainLoop()