]> git.saurik.com Git - wxWidgets.git/blame - wxPython/wx/lib/evtmgr.py
Patch from Will Sadkin:
[wxWidgets.git] / wxPython / wx / lib / evtmgr.py
CommitLineData
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 20A module that allows multiple handlers to respond to single wxWidgets
d14a1e28
RD
21events. This allows true NxN Observer/Observable connections: One
22event can be received by multiple handlers, and one handler can
23receive multiple events.
1fded56b 24
d14a1e28 25There are two ways to register event handlers. The first way is
4e5d278c 26similar 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
31There's also a new object-oriented way to register for events. This
32invocation is equivalent to the one above, but does not require the
4e5d278c 33programmer to declare or track control ids or parent containers::
d14a1e28
RD
34
35 eventManager.Register(handleEvents, EVT_BUTTON, myButton)
36
37This module is Python 2.1+ compatible.
38
39"""
b881fc78
RD
40import wx
41import pubsub # publish / subscribe library
d14a1e28
RD
42
43#---------------------------------------------------------------------------
44
45
46class 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):
e0fe229b 277 return isinstance(anObject, wx._core._wxPyDeadObject)
d14a1e28
RD
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
317class 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
384class 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
401class 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
455class 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?
493eventManager = EventManager()
494
495
496#---------------------------------------------------------------------------
497# simple test code
498
499
500if __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()