]> git.saurik.com Git - wxWidgets.git/blame - wxPython/wx/lib/evtmgr.py
Version number
[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#---------------------------------------------------------------------------
1fded56b 14
d14a1e28
RD
15"""
16A module that allows multiple handlers to respond to single wxWindows
17events. This allows true NxN Observer/Observable connections: One
18event can be received by multiple handlers, and one handler can
19receive multiple events.
1fded56b 20
d14a1e28
RD
21There are two ways to register event handlers. The first way is
22similar to standard wxPython handler registration:
23
24 from wxPython.lib.evtmgr import eventManager
25 eventManager.Register(handleEvents, EVT_BUTTON, win=frame, id=101)
26
27There's also a new object-oriented way to register for events. This
28invocation is equivalent to the one above, but does not require the
29programmer to declare or track control ids or parent containers:
30
31 eventManager.Register(handleEvents, EVT_BUTTON, myButton)
32
33This module is Python 2.1+ compatible.
34
35"""
36from wxPython import wx
37import pubsub
38
39#---------------------------------------------------------------------------
40
41
42class 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
300class 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
367class 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
384class 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
438class 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?
476eventManager = EventManager()
477
478
479#---------------------------------------------------------------------------
480# simple test code
481
482
483if __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()