]> git.saurik.com Git - wxWidgets.git/blob - wxPython/wxPython/lib/evtmgr.py
ef6fab004db0517374de65846cc6a829d3e04803
[wxWidgets.git] / wxPython / wxPython / 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) 2002 by Robb Shecter <robb@acm.org>
12 # Licence: wxWindows license
13 #---------------------------------------------------------------------------
14
15 """
16 A module that allows multiple handlers to respond to single wxWindows
17 events. This allows true NxN Observer/Observable connections: One
18 event can be received by multiple handlers, and one handler can
19 receive multiple events.
20
21 There are two ways to register event handlers. The first way is
22 similar to standard wxPython handler registration:
23
24 from wxPython.lib.evtmgr import eventManager
25 eventManager.Register(handleEvents, EVT_BUTTON, win=frame, id=101)
26
27 There's also a new object-oriented way to register for events. This
28 invocation is equivalent to the one above, but does not require the
29 programmer to declare or track control ids or parent containers:
30
31 eventManager.Register(handleEvents, EVT_BUTTON, myButton)
32
33 This module is Python 2.1+ compatible.
34
35 Author: Robb Shecter
36 """
37 from wxPython import wx
38 import pubsub
39
40 #---------------------------------------------------------------------------
41
42
43 class EventManager:
44 """
45 This is the main class in the module, and is the only class that
46 the application programmer needs to use. There is a pre-created
47 instance of this class called 'eventManager'. It should not be
48 necessary to create other instances.
49 """
50 def __init__(self):
51 self.eventAdapterDict = {}
52 self.messageAdapterDict = {}
53 self.windowTopicLookup = {}
54 self.listenerTopicLookup = {}
55 self.__publisher = pubsub.Publisher()
56 self.EMPTY_LIST = []
57
58
59 def Register(self, listener, event, source=None, win=None, id=None):
60 """
61 Registers a listener function (or any callable object) to
62 receive events of type event coming from the source window.
63 For example:
64
65 eventManager.Register(self.OnButton, EVT_BUTTON, theButton)
66
67 Alternatively, the specific window where the event is
68 delivered, and/or the ID of the event source can be specified.
69 For example:
70
71 eventManager.Register(self.OnButton, EVT_BUTTON, win=self, id=ID_BUTTON)
72 or
73 eventManager.Register(self.OnButton, EVT_BUTTON, theButton, self)
74 """
75
76 # 1. Check if the 'event' is actually one of the multi-
77 # event macros.
78 if _macroInfo.isMultiEvent(event):
79 raise 'Cannot register the macro, '+`event`+'. Register instead the individual events.'
80
81 # Support a more OO API. This allows the GUI widget itself to
82 # be specified, and the id to be retrieved from the system,
83 # instead of kept track of explicitly by the programmer.
84 # (Being used to doing GUI work with Java, this seems to me to be
85 # the natural way of doing things.)
86 if source is not None:
87 id = source.GetId()
88 if win is None:
89 # Some widgets do not function as their own windows.
90 win = self._determineWindow(source)
91 topic = (event, win, id)
92
93 # Create an adapter from the PS system back to wxEvents, and
94 # possibly one from wxEvents:
95 if not self.__haveMessageAdapter(listener, topic):
96 messageAdapter = MessageAdapter(eventHandler=listener, topicPattern=topic)
97 try:
98 self.messageAdapterDict[topic][listener] = messageAdapter
99 except KeyError:
100 self.messageAdapterDict[topic] = {}
101 self.messageAdapterDict[topic][listener] = messageAdapter
102
103 if not self.eventAdapterDict.has_key(topic):
104 self.eventAdapterDict[topic] = EventAdapter(event, win, id)
105 else:
106 # Throwing away a duplicate request
107 pass
108
109 # For time efficiency when deregistering by window:
110 try:
111 self.windowTopicLookup[win].append(topic)
112 except KeyError:
113 self.windowTopicLookup[win] = []
114 self.windowTopicLookup[win].append(topic)
115
116 # For time efficiency when deregistering by listener:
117 try:
118 self.listenerTopicLookup[listener].append(topic)
119 except KeyError:
120 self.listenerTopicLookup[listener] = []
121 self.listenerTopicLookup[listener].append(topic)
122
123 # See if the source understands the listeningFor protocol.
124 # This is a bit of a test I'm working on - it allows classes
125 # to know when their events are being listened to. I use
126 # it to enable chaining events from contained windows only
127 # when needed.
128 if source is not None:
129 try:
130 # Let the source know that we're listening for this
131 # event.
132 source.listeningFor(event)
133 except AttributeError:
134 pass
135
136 # Some aliases for Register, just for kicks
137 Bind = Register
138 Subscribe = Register
139
140
141 def DeregisterWindow(self, win):
142 """
143 Deregister all events coming from the given window.
144 """
145 win = self._determineWindow(win)
146 topics = self.__getTopics(win)
147 if topics:
148 for aTopic in topics:
149 self.__deregisterTopic(aTopic)
150 del self.windowTopicLookup[win]
151
152
153 def DeregisterListener(self, listener):
154 """
155 Deregister all event notifications for the given listener.
156 """
157 try:
158 topicList = self.listenerTopicLookup[listener]
159 except KeyError:
160 return
161
162 for topic in topicList:
163 topicDict = self.messageAdapterDict[topic]
164 if topicDict.has_key(listener):
165 topicDict[listener].Destroy()
166 del topicDict[listener]
167 if len(topicDict) == 0:
168 self.eventAdapterDict[topic].Destroy()
169 del self.eventAdapterDict[topic]
170 del self.messageAdapterDict[topic]
171 del self.listenerTopicLookup[listener]
172
173
174 def GetStats(self):
175 """
176 Return a dictionary with data about my state.
177 """
178 stats = {}
179 stats['Adapters: Message'] = reduce(lambda x,y: x+y, map(len, self.messageAdapterDict.values()))
180 stats['Adapters: Event'] = len(self.eventAdapterDict)
181 stats['Topics: Total'] = len(self.__getTopics())
182 stats['Topics: Dead'] = len(self.GetDeadTopics())
183 return stats
184
185
186 def DeregisterDeadTopics(self):
187 """
188 Deregister any entries relating to dead
189 wxPython objects. Not sure if this is an
190 important issue; 1) My app code always de-registers
191 listeners it doesn't need. 2) I don't think
192 that lingering references to these dead objects
193 is a problem.
194 """
195 for topic in self.GetDeadTopics():
196 self.DeregisterTopic(topic)
197
198
199 def GetDeadTopics(self):
200 """
201 Return a list of topics relating to dead wxPython
202 objects.
203 """
204 return filter(self.__isDeadTopic, self.__getTopics())
205
206
207 def __winString(self, aWin):
208 """
209 A string rep of a window for debugging
210 """
211 try:
212 name = aWin.GetClassName()
213 i = id(aWin)
214 return '%s #%d' % (name, i)
215 except wx.wxPyDeadObjectError:
216 return '(dead wxObject)'
217
218
219 def __topicString(self, aTopic):
220 """
221 A string rep of a topic for debugging
222 """
223 return '[%-26s %s]' % (aTopic[0].__name__, self.winString(aTopic[1]))
224
225
226 def __listenerString(self, aListener):
227 """
228 A string rep of a listener for debugging
229 """
230 try:
231 return aListener.im_class.__name__ + '.' + aListener.__name__
232 except:
233 return 'Function ' + aListener.__name__
234
235
236 def __deregisterTopic(self, aTopic):
237 try:
238 messageAdapterList = self.messageAdapterDict[aTopic].values()
239 except KeyError:
240 # This topic isn't valid. Probably because it was deleted
241 # by listener.
242 return
243 for messageAdapter in messageAdapterList:
244 messageAdapter.Destroy()
245 self.eventAdapterDict[aTopic].Destroy()
246 del self.messageAdapterDict[aTopic]
247 del self.eventAdapterDict[aTopic]
248
249
250 def __getTopics(self, win=None):
251 if win is None:
252 return self.messageAdapterDict.keys()
253 if win is not None:
254 try:
255 return self.windowTopicLookup[win]
256 except KeyError:
257 return self.EMPTY_LIST
258
259
260 def __isDeadWxObject(self, anObject):
261 return isinstance(anObject, wx._wxPyDeadObject)
262
263
264 def __isDeadTopic(self, aTopic):
265 return self.__isDeadWxObject(aTopic[1])
266
267
268 def __haveMessageAdapter(self, eventHandler, topicPattern):
269 """
270 Return True if there's already a message adapter
271 with these specs.
272 """
273 try:
274 return self.messageAdapterDict[topicPattern].has_key(eventHandler)
275 except KeyError:
276 return 0
277
278
279 def _determineWindow(self, aComponent):
280 """
281 Return the window that corresponds to this component.
282 A window is something that supports the Connect protocol.
283 Most things registered with the event manager are a window,
284 but there are apparently some exceptions. If more are
285 discovered, the implementation can be changed to a dictionary
286 lookup along the lines of class : function-to-get-window.
287 """
288 if isinstance(aComponent, wx.wxMenuItem):
289 return aComponent.GetMenu()
290 else:
291 return aComponent
292
293
294
295 #---------------------------------------------------------------------------
296 # From here down is implementaion and support classes, although you may
297 # find some of them useful in other contexts.
298 #---------------------------------------------------------------------------
299
300
301 class EventMacroInfo:
302 """
303 A class that provides information about event macros.
304 """
305 def __init__(self):
306 self.lookupTable = {}
307
308
309 def getEventTypes(self, eventMacro):
310 """
311 Return the list of event types that the given
312 macro corresponds to.
313 """
314 try:
315 return self.lookupTable[eventMacro]
316 except KeyError:
317 win = FakeWindow()
318 try:
319 eventMacro(win, None, None)
320 except TypeError:
321 eventMacro(win, None)
322 self.lookupTable[eventMacro] = win.eventTypes
323 return win.eventTypes
324
325
326 def eventIsA(self, event, macroList):
327 """
328 Return True if the event is one of the given
329 macros.
330 """
331 eventType = event.GetEventType()
332 for macro in macroList:
333 if eventType in self.getEventTypes(macro):
334 return 1
335 return 0
336
337
338 def macroIsA(self, macro, macroList):
339 """
340 Return True if the macro is in the macroList.
341 The added value of this method is that it takes
342 multi-events into account. The macroList parameter
343 will be coerced into a sequence if needed.
344 """
345 if callable(macroList):
346 macroList = (macroList,)
347 testList = self.getEventTypes(macro)
348 eventList = []
349 for m in macroList:
350 eventList.extend(self.getEventTypes(m))
351 # Return True if every element in testList is in eventList
352 for element in testList:
353 if element not in eventList:
354 return 0
355 return 1
356
357
358 def isMultiEvent(self, macro):
359 """
360 Return True if the given macro actually causes
361 multiple events to be registered.
362 """
363 return len(self.getEventTypes(macro)) > 1
364
365
366 #---------------------------------------------------------------------------
367
368 class FakeWindow:
369 """
370 Used internally by the EventMacroInfo class. The FakeWindow is
371 the most important component of the macro-info utility: it
372 implements the Connect() protocol of wxWindow, but instead of
373 registering for events, it keeps track of what parameters were
374 passed to it.
375 """
376 def __init__(self):
377 self.eventTypes = []
378
379 def Connect(self, id1, id2, eventType, handlerFunction):
380 self.eventTypes.append(eventType)
381
382
383 #---------------------------------------------------------------------------
384
385 class EventAdapter:
386 """
387 A class that adapts incoming wxWindows events to
388 Publish/Subscribe messages.
389
390 In other words, this is the object that's seen by the
391 wxWindows system. Only one of these registers for any
392 particular wxWindows event. It then relays it into the
393 PS system, which lets many listeners respond.
394 """
395 def __init__(self, func, win, id):
396 """
397 Instantiate a new adapter. Pre-compute my Publish/Subscribe
398 topic, which is constant, and register with wxWindows.
399 """
400 self.publisher = pubsub.Publisher()
401 self.topic = ((func, win, id),)
402 self.id = id
403 self.win = win
404 self.eventType = _macroInfo.getEventTypes(func)[0]
405
406 # Register myself with the wxWindows event system
407 try:
408 func(win, id, self.handleEvent)
409 self.callStyle = 3
410 except TypeError:
411 func(win, self.handleEvent)
412 self.callStyle = 2
413
414
415 def disconnect(self):
416 if self.callStyle == 3:
417 return self.win.Disconnect(self.id, -1, self.eventType)
418 else:
419 return self.win.Disconnect(-1, -1, self.eventType)
420
421
422 def handleEvent(self, event):
423 """
424 In response to a wxWindows event, send a PS message
425 """
426 self.publisher.sendMessage(topic=self.topic, data=event)
427
428
429 def Destroy(self):
430 try:
431 if not self.disconnect():
432 print 'disconnect failed'
433 except wx.wxPyDeadObjectError:
434 print 'disconnect failed: dead object' ##????
435
436
437 #---------------------------------------------------------------------------
438
439 class MessageAdapter:
440 """
441 A class that adapts incoming Publish/Subscribe messages
442 to wxWindows event calls.
443
444 This class works opposite the EventAdapter, and
445 retrieves the information an EventAdapter has sent in a message.
446 Strictly speaking, this class is not required: Event listeners
447 could pull the original wxEvent object out of the PS Message
448 themselves.
449
450 However, by pairing an instance of this class with each wxEvent
451 handler, the handlers can use the standard API: they receive an
452 event as a parameter.
453 """
454 def __init__(self, eventHandler, topicPattern):
455 """
456 Instantiate a new MessageAdapter that send wxEvents to the
457 given eventHandler.
458 """
459 self.eventHandler = eventHandler
460 pubsub.Publisher().subscribe(listener=self.deliverEvent, topic=(topicPattern,))
461
462 def deliverEvent(self, message):
463 event = message.data # Extract the wxEvent
464 self.eventHandler(event) # Perform the call as wxWindows would
465
466 def Destroy(self):
467 pubsub.Publisher().unsubscribe(listener=self.deliverEvent)
468
469
470 #---------------------------------------------------------------------------
471 # Create globals
472
473 _macroInfo = EventMacroInfo()
474
475 # For now a singleton is not enforced. Should it be or can we trust
476 # the programmers?
477 eventManager = EventManager()
478
479
480 #---------------------------------------------------------------------------
481 # simple test code
482
483
484 if __name__ == '__main__':
485 from wxPython.wx import wxPySimpleApp, wxFrame, wxToggleButton, wxBoxSizer, wxHORIZONTAL, EVT_MOTION, EVT_LEFT_DOWN, EVT_TOGGLEBUTTON, wxALL
486 app = wxPySimpleApp()
487 frame = wxFrame(None, -1, 'Event Test', size=(300,300))
488 button = wxToggleButton(frame, -1, 'Listen for Mouse Events')
489 sizer = wxBoxSizer(wxHORIZONTAL)
490 sizer.Add(button, 0, 0 | wxALL, 10)
491 frame.SetAutoLayout(1)
492 frame.SetSizer(sizer)
493
494 #
495 # Demonstrate 1) register/deregister, 2) Multiple listeners receiving
496 # one event, and 3) Multiple events going to one listener.
497 #
498
499 def printEvent(event):
500 print 'Name:',event.GetClassName(),'Timestamp',event.GetTimestamp()
501
502 def enableFrameEvents(event):
503 # Turn the output of mouse events on and off
504 if event.IsChecked():
505 print '\nEnabling mouse events...'
506 eventManager.Register(printEvent, EVT_MOTION, frame)
507 eventManager.Register(printEvent, EVT_LEFT_DOWN, frame)
508 else:
509 print '\nDisabling mouse events...'
510 eventManager.DeregisterWindow(frame)
511
512 # Send togglebutton events to both the on/off code as well
513 # as the function that prints to stdout.
514 eventManager.Register(printEvent, EVT_TOGGLEBUTTON, button)
515 eventManager.Register(enableFrameEvents, EVT_TOGGLEBUTTON, button)
516
517 frame.CenterOnScreen()
518 frame.Show(1)
519 app.MainLoop()