]> git.saurik.com Git - wxWidgets.git/blame - wxPython/wx/lib/evtmgr.py
A little cleanup
[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
RD
19"""
20A module that allows multiple handlers to respond to single wxWindows
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
RD
25There are two ways to register event handlers. The first way is
26similar to standard wxPython handler registration:
27
28 from wxPython.lib.evtmgr import eventManager
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
33programmer to declare or track control ids or parent containers:
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::
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
314class 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
381class 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
398class 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
452class 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?
490eventManager = EventManager()
491
492
493#---------------------------------------------------------------------------
494# simple test code
495
496
497if __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()