]>
git.saurik.com Git - wxWidgets.git/blob - wxPython/wxPython/lib/evtmgr.py
ef6fab004db0517374de65846cc6a829d3e04803
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) 2002 by Robb Shecter <robb@acm.org>
12 # Licence: wxWindows license
13 #---------------------------------------------------------------------------
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.
21 There are two ways to register event handlers. The first way is
22 similar to standard wxPython handler registration:
24 from wxPython.lib.evtmgr import eventManager
25 eventManager.Register(handleEvents, EVT_BUTTON, win=frame, id=101)
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:
31 eventManager.Register(handleEvents, EVT_BUTTON, myButton)
33 This module is Python 2.1+ compatible.
37 from wxPython
import wx
40 #---------------------------------------------------------------------------
45 This is the main class in the module, and is the only class that
46 the application programmer needs to use. There is a pre-created
47 instance of this class called 'eventManager'. It should not be
48 necessary to create other instances.
51 self
.eventAdapterDict
= {}
52 self
.messageAdapterDict
= {}
53 self
.windowTopicLookup
= {}
54 self
.listenerTopicLookup
= {}
55 self
.__publisher
= pubsub
.Publisher()
59 def Register(self
, listener
, event
, source
=None, win
=None, id=None):
61 Registers a listener function (or any callable object) to
62 receive events of type event coming from the source window.
65 eventManager.Register(self.OnButton, EVT_BUTTON, theButton)
67 Alternatively, the specific window where the event is
68 delivered, and/or the ID of the event source can be specified.
71 eventManager.Register(self.OnButton, EVT_BUTTON, win=self, id=ID_BUTTON)
73 eventManager.Register(self.OnButton, EVT_BUTTON, theButton, self)
76 # 1. Check if the 'event' is actually one of the multi-
78 if _macroInfo
.isMultiEvent(event
):
79 raise 'Cannot register the macro, '+`event`
+'. Register instead the individual events.'
81 # Support a more OO API. This allows the GUI widget itself to
82 # be specified, and the id to be retrieved from the system,
83 # instead of kept track of explicitly by the programmer.
84 # (Being used to doing GUI work with Java, this seems to me to be
85 # the natural way of doing things.)
86 if source
is not None:
89 # Some widgets do not function as their own windows.
90 win
= self
._determineWindow
(source
)
91 topic
= (event
, win
, id)
93 # Create an adapter from the PS system back to wxEvents, and
94 # possibly one from wxEvents:
95 if not self
.__haveMessageAdapter
(listener
, topic
):
96 messageAdapter
= MessageAdapter(eventHandler
=listener
, topicPattern
=topic
)
98 self
.messageAdapterDict
[topic
][listener
] = messageAdapter
100 self
.messageAdapterDict
[topic
] = {}
101 self
.messageAdapterDict
[topic
][listener
] = messageAdapter
103 if not self
.eventAdapterDict
.has_key(topic
):
104 self
.eventAdapterDict
[topic
] = EventAdapter(event
, win
, id)
106 # Throwing away a duplicate request
109 # For time efficiency when deregistering by window:
111 self
.windowTopicLookup
[win
].append(topic
)
113 self
.windowTopicLookup
[win
] = []
114 self
.windowTopicLookup
[win
].append(topic
)
116 # For time efficiency when deregistering by listener:
118 self
.listenerTopicLookup
[listener
].append(topic
)
120 self
.listenerTopicLookup
[listener
] = []
121 self
.listenerTopicLookup
[listener
].append(topic
)
123 # See if the source understands the listeningFor protocol.
124 # This is a bit of a test I'm working on - it allows classes
125 # to know when their events are being listened to. I use
126 # it to enable chaining events from contained windows only
128 if source
is not None:
130 # Let the source know that we're listening for this
132 source
.listeningFor(event
)
133 except AttributeError:
136 # Some aliases for Register, just for kicks
141 def DeregisterWindow(self
, win
):
143 Deregister all events coming from the given window.
145 win
= self
._determineWindow
(win
)
146 topics
= self
.__getTopics
(win
)
148 for aTopic
in topics
:
149 self
.__deregisterTopic
(aTopic
)
150 del self
.windowTopicLookup
[win
]
153 def DeregisterListener(self
, listener
):
155 Deregister all event notifications for the given listener.
158 topicList
= self
.listenerTopicLookup
[listener
]
162 for topic
in topicList
:
163 topicDict
= self
.messageAdapterDict
[topic
]
164 if topicDict
.has_key(listener
):
165 topicDict
[listener
].Destroy()
166 del topicDict
[listener
]
167 if len(topicDict
) == 0:
168 self
.eventAdapterDict
[topic
].Destroy()
169 del self
.eventAdapterDict
[topic
]
170 del self
.messageAdapterDict
[topic
]
171 del self
.listenerTopicLookup
[listener
]
176 Return a dictionary with data about my state.
179 stats
['Adapters: Message'] = reduce(lambda x
,y
: x
+y
, map(len, self
.messageAdapterDict
.values()))
180 stats
['Adapters: Event'] = len(self
.eventAdapterDict
)
181 stats
['Topics: Total'] = len(self
.__getTopics
())
182 stats
['Topics: Dead'] = len(self
.GetDeadTopics())
186 def DeregisterDeadTopics(self
):
188 Deregister any entries relating to dead
189 wxPython objects. Not sure if this is an
190 important issue; 1) My app code always de-registers
191 listeners it doesn't need. 2) I don't think
192 that lingering references to these dead objects
195 for topic
in self
.GetDeadTopics():
196 self
.DeregisterTopic(topic
)
199 def GetDeadTopics(self
):
201 Return a list of topics relating to dead wxPython
204 return filter(self
.__isDeadTopic
, self
.__getTopics
())
207 def __winString(self
, aWin
):
209 A string rep of a window for debugging
212 name
= aWin
.GetClassName()
214 return '%s #%d' % (name
, i
)
215 except wx
.wxPyDeadObjectError
:
216 return '(dead wxObject)'
219 def __topicString(self
, aTopic
):
221 A string rep of a topic for debugging
223 return '[%-26s %s]' % (aTopic
[0].__name
__, self
.winString(aTopic
[1]))
226 def __listenerString(self
, aListener
):
228 A string rep of a listener for debugging
231 return aListener
.im_class
.__name
__ + '.' + aListener
.__name
__
233 return 'Function ' + aListener
.__name
__
236 def __deregisterTopic(self
, aTopic
):
238 messageAdapterList
= self
.messageAdapterDict
[aTopic
].values()
240 # This topic isn't valid. Probably because it was deleted
243 for messageAdapter
in messageAdapterList
:
244 messageAdapter
.Destroy()
245 self
.eventAdapterDict
[aTopic
].Destroy()
246 del self
.messageAdapterDict
[aTopic
]
247 del self
.eventAdapterDict
[aTopic
]
250 def __getTopics(self
, win
=None):
252 return self
.messageAdapterDict
.keys()
255 return self
.windowTopicLookup
[win
]
257 return self
.EMPTY_LIST
260 def __isDeadWxObject(self
, anObject
):
261 return isinstance(anObject
, wx
._wxPyDeadObject
)
264 def __isDeadTopic(self
, aTopic
):
265 return self
.__isDeadWxObject
(aTopic
[1])
268 def __haveMessageAdapter(self
, eventHandler
, topicPattern
):
270 Return True if there's already a message adapter
274 return self
.messageAdapterDict
[topicPattern
].has_key(eventHandler
)
279 def _determineWindow(self
, aComponent
):
281 Return the window that corresponds to this component.
282 A window is something that supports the Connect protocol.
283 Most things registered with the event manager are a window,
284 but there are apparently some exceptions. If more are
285 discovered, the implementation can be changed to a dictionary
286 lookup along the lines of class : function-to-get-window.
288 if isinstance(aComponent
, wx
.wxMenuItem
):
289 return aComponent
.GetMenu()
295 #---------------------------------------------------------------------------
296 # From here down is implementaion and support classes, although you may
297 # find some of them useful in other contexts.
298 #---------------------------------------------------------------------------
301 class EventMacroInfo
:
303 A class that provides information about event macros.
306 self
.lookupTable
= {}
309 def getEventTypes(self
, eventMacro
):
311 Return the list of event types that the given
312 macro corresponds to.
315 return self
.lookupTable
[eventMacro
]
319 eventMacro(win
, None, None)
321 eventMacro(win
, None)
322 self
.lookupTable
[eventMacro
] = win
.eventTypes
323 return win
.eventTypes
326 def eventIsA(self
, event
, macroList
):
328 Return True if the event is one of the given
331 eventType
= event
.GetEventType()
332 for macro
in macroList
:
333 if eventType
in self
.getEventTypes(macro
):
338 def macroIsA(self
, macro
, macroList
):
340 Return True if the macro is in the macroList.
341 The added value of this method is that it takes
342 multi-events into account. The macroList parameter
343 will be coerced into a sequence if needed.
345 if callable(macroList
):
346 macroList
= (macroList
,)
347 testList
= self
.getEventTypes(macro
)
350 eventList
.extend(self
.getEventTypes(m
))
351 # Return True if every element in testList is in eventList
352 for element
in testList
:
353 if element
not in eventList
:
358 def isMultiEvent(self
, macro
):
360 Return True if the given macro actually causes
361 multiple events to be registered.
363 return len(self
.getEventTypes(macro
)) > 1
366 #---------------------------------------------------------------------------
370 Used internally by the EventMacroInfo class. The FakeWindow is
371 the most important component of the macro-info utility: it
372 implements the Connect() protocol of wxWindow, but instead of
373 registering for events, it keeps track of what parameters were
379 def Connect(self
, id1
, id2
, eventType
, handlerFunction
):
380 self
.eventTypes
.append(eventType
)
383 #---------------------------------------------------------------------------
387 A class that adapts incoming wxWindows events to
388 Publish/Subscribe messages.
390 In other words, this is the object that's seen by the
391 wxWindows system. Only one of these registers for any
392 particular wxWindows event. It then relays it into the
393 PS system, which lets many listeners respond.
395 def __init__(self
, func
, win
, id):
397 Instantiate a new adapter. Pre-compute my Publish/Subscribe
398 topic, which is constant, and register with wxWindows.
400 self
.publisher
= pubsub
.Publisher()
401 self
.topic
= ((func
, win
, id),)
404 self
.eventType
= _macroInfo
.getEventTypes(func
)[0]
406 # Register myself with the wxWindows event system
408 func(win
, id, self
.handleEvent
)
411 func(win
, self
.handleEvent
)
415 def disconnect(self
):
416 if self
.callStyle
== 3:
417 return self
.win
.Disconnect(self
.id, -1, self
.eventType
)
419 return self
.win
.Disconnect(-1, -1, self
.eventType
)
422 def handleEvent(self
, event
):
424 In response to a wxWindows event, send a PS message
426 self
.publisher
.sendMessage(topic
=self
.topic
, data
=event
)
431 if not self
.disconnect():
432 print 'disconnect failed'
433 except wx
.wxPyDeadObjectError
:
434 print 'disconnect failed: dead object' ##????
437 #---------------------------------------------------------------------------
439 class MessageAdapter
:
441 A class that adapts incoming Publish/Subscribe messages
442 to wxWindows event calls.
444 This class works opposite the EventAdapter, and
445 retrieves the information an EventAdapter has sent in a message.
446 Strictly speaking, this class is not required: Event listeners
447 could pull the original wxEvent object out of the PS Message
450 However, by pairing an instance of this class with each wxEvent
451 handler, the handlers can use the standard API: they receive an
452 event as a parameter.
454 def __init__(self
, eventHandler
, topicPattern
):
456 Instantiate a new MessageAdapter that send wxEvents to the
459 self
.eventHandler
= eventHandler
460 pubsub
.Publisher().subscribe(listener
=self
.deliverEvent
, topic
=(topicPattern
,))
462 def deliverEvent(self
, message
):
463 event
= message
.data
# Extract the wxEvent
464 self
.eventHandler(event
) # Perform the call as wxWindows would
467 pubsub
.Publisher().unsubscribe(listener
=self
.deliverEvent
)
470 #---------------------------------------------------------------------------
473 _macroInfo
= EventMacroInfo()
475 # For now a singleton is not enforced. Should it be or can we trust
477 eventManager
= EventManager()
480 #---------------------------------------------------------------------------
484 if __name__
== '__main__':
485 from wxPython
.wx
import wxPySimpleApp
, wxFrame
, wxToggleButton
, wxBoxSizer
, wxHORIZONTAL
, EVT_MOTION
, EVT_LEFT_DOWN
, EVT_TOGGLEBUTTON
, wxALL
486 app
= wxPySimpleApp()
487 frame
= wxFrame(None, -1, 'Event Test', size
=(300,300))
488 button
= wxToggleButton(frame
, -1, 'Listen for Mouse Events')
489 sizer
= wxBoxSizer(wxHORIZONTAL
)
490 sizer
.Add(button
, 0, 0 | wxALL
, 10)
491 frame
.SetAutoLayout(1)
492 frame
.SetSizer(sizer
)
495 # Demonstrate 1) register/deregister, 2) Multiple listeners receiving
496 # one event, and 3) Multiple events going to one listener.
499 def printEvent(event
):
500 print 'Name:',event
.GetClassName(),'Timestamp',event
.GetTimestamp()
502 def enableFrameEvents(event
):
503 # Turn the output of mouse events on and off
504 if event
.IsChecked():
505 print '\nEnabling mouse events...'
506 eventManager
.Register(printEvent
, EVT_MOTION
, frame
)
507 eventManager
.Register(printEvent
, EVT_LEFT_DOWN
, frame
)
509 print '\nDisabling mouse events...'
510 eventManager
.DeregisterWindow(frame
)
512 # Send togglebutton events to both the on/off code as well
513 # as the function that prints to stdout.
514 eventManager
.Register(printEvent
, EVT_TOGGLEBUTTON
, button
)
515 eventManager
.Register(enableFrameEvents
, EVT_TOGGLEBUTTON
, button
)
517 frame
.CenterOnScreen()