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