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