]>
git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/evtmgr.py
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
7 # Author: Robb Shecter and Robin Dunn
9 # Created: 12-December-2002
11 # Copyright: (c) 2003 by db-X Corporation
12 # Licence: wxWindows license
13 #---------------------------------------------------------------------------
14 # 12/02/2003 - Jeff Grimmett (grimmtooth@softhome.net)
16 # o Updated for 2.5 compatability.
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.
25 There are two ways to register event handlers. The first way is
26 similar to standard wxPython handler registration:
28 from wxPython.lib.evtmgr import eventManager
29 eventManager.Register(handleEvents, EVT_BUTTON, win=frame, id=101)
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:
35 eventManager.Register(handleEvents, EVT_BUTTON, myButton)
37 This module is Python 2.1+ compatible.
41 import pubsub
# publish / subscribe library
43 #---------------------------------------------------------------------------
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.
54 self
.eventAdapterDict
= {}
55 self
.messageAdapterDict
= {}
56 self
.windowTopicLookup
= {}
57 self
.listenerTopicLookup
= {}
58 self
.__publisher
= pubsub
.Publisher()
62 def Register(self
, listener
, event
, source
=None, win
=None, id=None):
64 Registers a listener function (or any callable object) to
65 receive events of type event coming from the source window.
67 eventManager.Register(self.OnButton, EVT_BUTTON, theButton)
69 Alternatively, the specific window where the event is
70 delivered, and/or the ID of the event source can be specified.
72 eventManager.Register(self.OnButton, EVT_BUTTON, win=self, id=ID_BUTTON)
75 eventManager.Register(self.OnButton, EVT_BUTTON, theButton, self)
79 # 1. Check if the 'event' is actually one of the multi-
81 if _macroInfo
.isMultiEvent(event
):
82 raise 'Cannot register the macro, '+`event`
+'. Register instead the individual events.'
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:
93 # Some widgets do not function as their own windows.
94 win
= self
._determineWindow
(source
)
96 topic
= (event
, win
, id)
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
)
103 self
.messageAdapterDict
[topic
][listener
] = messageAdapter
105 self
.messageAdapterDict
[topic
] = {}
106 self
.messageAdapterDict
[topic
][listener
] = messageAdapter
108 if not self
.eventAdapterDict
.has_key(topic
):
109 self
.eventAdapterDict
[topic
] = EventAdapter(event
, win
, id)
111 # Throwing away a duplicate request
114 # For time efficiency when deregistering by window:
116 self
.windowTopicLookup
[win
].append(topic
)
118 self
.windowTopicLookup
[win
] = []
119 self
.windowTopicLookup
[win
].append(topic
)
121 # For time efficiency when deregistering by listener:
123 self
.listenerTopicLookup
[listener
].append(topic
)
125 self
.listenerTopicLookup
[listener
] = []
126 self
.listenerTopicLookup
[listener
].append(topic
)
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
133 if source
is not None:
135 # Let the source know that we're listening for this
137 source
.listeningFor(event
)
138 except AttributeError:
141 # Some aliases for Register, just for kicks
146 def DeregisterWindow(self
, win
):
148 Deregister all events coming from the given window.
150 win
= self
._determineWindow
(win
)
151 topics
= self
.__getTopics
(win
)
154 for aTopic
in topics
:
155 self
.__deregisterTopic
(aTopic
)
157 del self
.windowTopicLookup
[win
]
160 def DeregisterListener(self
, listener
):
162 Deregister all event notifications for the given listener.
165 topicList
= self
.listenerTopicLookup
[listener
]
169 for topic
in topicList
:
170 topicDict
= self
.messageAdapterDict
[topic
]
172 if topicDict
.has_key(listener
):
173 topicDict
[listener
].Destroy()
174 del topicDict
[listener
]
176 if len(topicDict
) == 0:
177 self
.eventAdapterDict
[topic
].Destroy()
178 del self
.eventAdapterDict
[topic
]
179 del self
.messageAdapterDict
[topic
]
181 del self
.listenerTopicLookup
[listener
]
186 Return a dictionary with data about my state.
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())
196 def DeregisterDeadTopics(self
):
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
205 for topic
in self
.GetDeadTopics():
206 self
.__deregisterTopic
(topic
)
209 def GetDeadTopics(self
):
211 Return a list of topics relating to dead wxPython
214 return filter(self
.__isDeadTopic
, self
.__getTopics
())
217 def __winString(self
, aWin
):
219 A string rep of a window for debugging
222 name
= aWin
.GetClassName()
224 return '%s #%d' % (name
, i
)
225 except wx
.PyDeadObjectError
:
226 return '(dead wx.Object)'
229 def __topicString(self
, aTopic
):
231 A string rep of a topic for debugging
233 return '[%-26s %s]' % (aTopic
[0].__name
__, self
.winString(aTopic
[1]))
236 def __listenerString(self
, aListener
):
238 A string rep of a listener for debugging
241 return aListener
.im_class
.__name
__ + '.' + aListener
.__name
__
243 return 'Function ' + aListener
.__name
__
246 def __deregisterTopic(self
, aTopic
):
248 messageAdapterList
= self
.messageAdapterDict
[aTopic
].values()
250 # This topic isn't valid. Probably because it was deleted
254 for messageAdapter
in messageAdapterList
:
255 messageAdapter
.Destroy()
257 self
.eventAdapterDict
[aTopic
].Destroy()
258 del self
.messageAdapterDict
[aTopic
]
259 del self
.eventAdapterDict
[aTopic
]
262 def __getTopics(self
, win
=None):
264 return self
.messageAdapterDict
.keys()
268 return self
.windowTopicLookup
[win
]
270 return self
.EMPTY_LIST
273 def __isDeadWxObject(self
, anObject
):
274 return isinstance(anObject
, wx
._wxPyDeadObject
)
277 def __isDeadTopic(self
, aTopic
):
278 return self
.__isDeadWxObject
(aTopic
[1])
281 def __haveMessageAdapter(self
, eventHandler
, topicPattern
):
283 Return True if there's already a message adapter
287 return self
.messageAdapterDict
[topicPattern
].has_key(eventHandler
)
292 def _determineWindow(self
, aComponent
):
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.
301 if isinstance(aComponent
, wx
.MenuItem
):
302 return aComponent
.GetMenu()
308 #---------------------------------------------------------------------------
309 # From here down is implementaion and support classes, although you may
310 # find some of them useful in other contexts.
311 #---------------------------------------------------------------------------
314 class EventMacroInfo
:
316 A class that provides information about event macros.
319 self
.lookupTable
= {}
322 def getEventTypes(self
, eventMacro
):
324 Return the list of event types that the given
325 macro corresponds to.
328 return self
.lookupTable
[eventMacro
]
332 eventMacro(win
, None, None)
333 except (TypeError, AssertionError):
334 eventMacro(win
, None)
335 self
.lookupTable
[eventMacro
] = win
.eventTypes
336 return win
.eventTypes
339 def eventIsA(self
, event
, macroList
):
341 Return True if the event is one of the given
344 eventType
= event
.GetEventType()
345 for macro
in macroList
:
346 if eventType
in self
.getEventTypes(macro
):
351 def macroIsA(self
, macro
, macroList
):
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.
358 if callable(macroList
):
359 macroList
= (macroList
,)
360 testList
= self
.getEventTypes(macro
)
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
:
371 def isMultiEvent(self
, macro
):
373 Return True if the given macro actually causes
374 multiple events to be registered.
376 return len(self
.getEventTypes(macro
)) > 1
379 #---------------------------------------------------------------------------
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
392 def Connect(self
, id1
, id2
, eventType
, handlerFunction
):
393 self
.eventTypes
.append(eventType
)
396 #---------------------------------------------------------------------------
400 A class that adapts incoming wxWindows events to
401 Publish/Subscribe messages.
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.
408 def __init__(self
, func
, win
, id):
410 Instantiate a new adapter. Pre-compute my Publish/Subscribe
411 topic, which is constant, and register with wxWindows.
413 self
.publisher
= pubsub
.Publisher()
414 self
.topic
= ((func
, win
, id),)
417 self
.eventType
= _macroInfo
.getEventTypes(func
)[0]
419 # Register myself with the wxWindows event system
421 func(win
, id, self
.handleEvent
)
423 except (TypeError, AssertionError):
424 func(win
, self
.handleEvent
)
428 def disconnect(self
):
429 if self
.callStyle
== 3:
430 return self
.win
.Disconnect(self
.id, -1, self
.eventType
)
432 return self
.win
.Disconnect(-1, -1, self
.eventType
)
435 def handleEvent(self
, event
):
437 In response to a wxWindows event, send a PS message
439 self
.publisher
.sendMessage(topic
=self
.topic
, data
=event
)
444 if not self
.disconnect():
445 print 'disconnect failed'
446 except wx
.PyDeadObjectError
:
447 print 'disconnect failed: dead object' ##????
450 #---------------------------------------------------------------------------
452 class MessageAdapter
:
454 A class that adapts incoming Publish/Subscribe messages
455 to wxWindows event calls.
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
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.
467 def __init__(self
, eventHandler
, topicPattern
):
469 Instantiate a new MessageAdapter that send wxEvents to the
472 self
.eventHandler
= eventHandler
473 pubsub
.Publisher().subscribe(listener
=self
.deliverEvent
, topic
=(topicPattern
,))
475 def deliverEvent(self
, message
):
476 event
= message
.data
# Extract the wxEvent
477 self
.eventHandler(event
) # Perform the call as wxWindows would
480 pubsub
.Publisher().unsubscribe(listener
=self
.deliverEvent
)
483 #---------------------------------------------------------------------------
486 _macroInfo
= EventMacroInfo()
488 # For now a singleton is not enforced. Should it be or can we trust
490 eventManager
= EventManager()
493 #---------------------------------------------------------------------------
497 if __name__
== '__main__':
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)
503 frame
.SetAutoLayout(1)
504 frame
.SetSizer(sizer
)
507 # Demonstrate 1) register/deregister, 2) Multiple listeners receiving
508 # one event, and 3) Multiple events going to one listener.
511 def printEvent(event
):
512 print 'Name:',event
.GetClassName(),'Timestamp',event
.GetTimestamp()
514 def enableFrameEvents(event
):
515 # Turn the output of mouse events on and off
516 if event
.IsChecked():
517 print '\nEnabling mouse events...'
518 eventManager
.Register(printEvent
, wx
.EVT_MOTION
, frame
)
519 eventManager
.Register(printEvent
, wx
.EVT_LEFT_DOWN
, frame
)
521 print '\nDisabling mouse events...'
522 eventManager
.DeregisterWindow(frame
)
524 # Send togglebutton events to both the on/off code as well
525 # as the function that prints to stdout.
526 eventManager
.Register(printEvent
, wx
.EVT_TOGGLEBUTTON
, button
)
527 eventManager
.Register(enableFrameEvents
, wx
.EVT_TOGGLEBUTTON
, button
)
529 frame
.CenterOnScreen()