]>
Commit | Line | Data |
---|---|---|
d14a1e28 RD |
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 | #--------------------------------------------------------------------------- | |
b881fc78 RD |
14 | # 12/02/2003 - Jeff Grimmett (grimmtooth@softhome.net) |
15 | # | |
16 | # o Updated for 2.5 compatability. | |
17 | # | |
1fded56b | 18 | |
d14a1e28 | 19 | """ |
4e5d278c | 20 | A module that allows multiple handlers to respond to single wxWidgets |
d14a1e28 RD |
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. | |
1fded56b | 24 | |
d14a1e28 | 25 | There are two ways to register event handlers. The first way is |
4e5d278c | 26 | similar to standard wxPython handler registration:: |
d14a1e28 | 27 | |
4e5d278c | 28 | from wx.lib.evtmgr import eventManager |
d14a1e28 RD |
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 | |
4e5d278c | 33 | programmer to declare or track control ids or parent containers:: |
d14a1e28 RD |
34 | |
35 | eventManager.Register(handleEvents, EVT_BUTTON, myButton) | |
36 | ||
37 | This module is Python 2.1+ compatible. | |
38 | ||
39 | """ | |
b881fc78 RD |
40 | import wx |
41 | import pubsub # publish / subscribe library | |
d14a1e28 RD |
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. | |
fdc775af | 66 | For example:: |
4e5d278c | 67 | |
d14a1e28 RD |
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. | |
fdc775af | 72 | For example:: |
4e5d278c | 73 | |
d14a1e28 | 74 | eventManager.Register(self.OnButton, EVT_BUTTON, win=self, id=ID_BUTTON) |
fdc775af RD |
75 | |
76 | or:: | |
4e5d278c | 77 | |
d14a1e28 | 78 | eventManager.Register(self.OnButton, EVT_BUTTON, theButton, self) |
fdc775af | 79 | |
d14a1e28 RD |
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() | |
b881fc78 | 94 | |
d14a1e28 RD |
95 | if win is None: |
96 | # Some widgets do not function as their own windows. | |
97 | win = self._determineWindow(source) | |
b881fc78 | 98 | |
d14a1e28 RD |
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) | |
b881fc78 | 155 | |
d14a1e28 RD |
156 | if topics: |
157 | for aTopic in topics: | |
158 | self.__deregisterTopic(aTopic) | |
b881fc78 | 159 | |
d14a1e28 RD |
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] | |
b881fc78 | 174 | |
d14a1e28 RD |
175 | if topicDict.has_key(listener): |
176 | topicDict[listener].Destroy() | |
177 | del topicDict[listener] | |
b881fc78 | 178 | |
d14a1e28 RD |
179 | if len(topicDict) == 0: |
180 | self.eventAdapterDict[topic].Destroy() | |
181 | del self.eventAdapterDict[topic] | |
182 | del self.messageAdapterDict[topic] | |
b881fc78 | 183 | |
d14a1e28 RD |
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) | |
b881fc78 RD |
228 | except wx.PyDeadObjectError: |
229 | return '(dead wx.Object)' | |
d14a1e28 RD |
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 | |
b881fc78 | 256 | |
d14a1e28 RD |
257 | for messageAdapter in messageAdapterList: |
258 | messageAdapter.Destroy() | |
b881fc78 | 259 | |
d14a1e28 RD |
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() | |
b881fc78 | 268 | |
d14a1e28 RD |
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._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 | """ | |
b881fc78 | 304 | if isinstance(aComponent, wx.MenuItem): |
d14a1e28 RD |
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) | |
fd3f2efe | 336 | except (TypeError, AssertionError): |
d14a1e28 RD |
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 | |
fd3f2efe | 426 | except (TypeError, AssertionError): |
d14a1e28 RD |
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' | |
b881fc78 | 449 | except wx.PyDeadObjectError: |
d14a1e28 RD |
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__': | |
b881fc78 RD |
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) | |
d14a1e28 RD |
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...' | |
b881fc78 RD |
521 | eventManager.Register(printEvent, wx.EVT_MOTION, frame) |
522 | eventManager.Register(printEvent, wx.EVT_LEFT_DOWN, frame) | |
d14a1e28 RD |
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. | |
b881fc78 RD |
529 | eventManager.Register(printEvent, wx.EVT_TOGGLEBUTTON, button) |
530 | eventManager.Register(enableFrameEvents, wx.EVT_TOGGLEBUTTON, button) | |
d14a1e28 RD |
531 | |
532 | frame.CenterOnScreen() | |
533 | frame.Show(1) | |
534 | app.MainLoop() |