]>
git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/evtmgr.py
4ea2795342926839d57586e373a9c48cf092ec4a
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 #---------------------------------------------------------------------------
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.
36 from wxPython
import wx
39 #---------------------------------------------------------------------------
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.
50 self
.eventAdapterDict
= {}
51 self
.messageAdapterDict
= {}
52 self
.windowTopicLookup
= {}
53 self
.listenerTopicLookup
= {}
54 self
.__publisher
= pubsub
.Publisher()
58 def Register(self
, listener
, event
, source
=None, win
=None, id=None):
60 Registers a listener function (or any callable object) to
61 receive events of type event coming from the source window.
64 eventManager.Register(self.OnButton, EVT_BUTTON, theButton)
66 Alternatively, the specific window where the event is
67 delivered, and/or the ID of the event source can be specified.
70 eventManager.Register(self.OnButton, EVT_BUTTON, win=self, id=ID_BUTTON)
72 eventManager.Register(self.OnButton, EVT_BUTTON, theButton, self)
75 # 1. Check if the 'event' is actually one of the multi-
77 if _macroInfo
.isMultiEvent(event
):
78 raise 'Cannot register the macro, '+`event`
+'. Register instead the individual events.'
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:
88 # Some widgets do not function as their own windows.
89 win
= self
._determineWindow
(source
)
90 topic
= (event
, win
, id)
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
)
97 self
.messageAdapterDict
[topic
][listener
] = messageAdapter
99 self
.messageAdapterDict
[topic
] = {}
100 self
.messageAdapterDict
[topic
][listener
] = messageAdapter
102 if not self
.eventAdapterDict
.has_key(topic
):
103 self
.eventAdapterDict
[topic
] = EventAdapter(event
, win
, id)
105 # Throwing away a duplicate request
108 # For time efficiency when deregistering by window:
110 self
.windowTopicLookup
[win
].append(topic
)
112 self
.windowTopicLookup
[win
] = []
113 self
.windowTopicLookup
[win
].append(topic
)
115 # For time efficiency when deregistering by listener:
117 self
.listenerTopicLookup
[listener
].append(topic
)
119 self
.listenerTopicLookup
[listener
] = []
120 self
.listenerTopicLookup
[listener
].append(topic
)
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
127 if source
is not None:
129 # Let the source know that we're listening for this
131 source
.listeningFor(event
)
132 except AttributeError:
135 # Some aliases for Register, just for kicks
140 def DeregisterWindow(self
, win
):
142 Deregister all events coming from the given window.
144 win
= self
._determineWindow
(win
)
145 topics
= self
.__getTopics
(win
)
147 for aTopic
in topics
:
148 self
.__deregisterTopic
(aTopic
)
149 del self
.windowTopicLookup
[win
]
152 def DeregisterListener(self
, listener
):
154 Deregister all event notifications for the given listener.
157 topicList
= self
.listenerTopicLookup
[listener
]
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
]
175 Return a dictionary with data about my state.
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())
185 def DeregisterDeadTopics(self
):
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
194 for topic
in self
.GetDeadTopics():
195 self
.__deregisterTopic
(topic
)
198 def GetDeadTopics(self
):
200 Return a list of topics relating to dead wxPython
203 return filter(self
.__isDeadTopic
, self
.__getTopics
())
206 def __winString(self
, aWin
):
208 A string rep of a window for debugging
211 name
= aWin
.GetClassName()
213 return '%s #%d' % (name
, i
)
214 except wx
.wxPyDeadObjectError
:
215 return '(dead wxObject)'
218 def __topicString(self
, aTopic
):
220 A string rep of a topic for debugging
222 return '[%-26s %s]' % (aTopic
[0].__name
__, self
.winString(aTopic
[1]))
225 def __listenerString(self
, aListener
):
227 A string rep of a listener for debugging
230 return aListener
.im_class
.__name
__ + '.' + aListener
.__name
__
232 return 'Function ' + aListener
.__name
__
235 def __deregisterTopic(self
, aTopic
):
237 messageAdapterList
= self
.messageAdapterDict
[aTopic
].values()
239 # This topic isn't valid. Probably because it was deleted
242 for messageAdapter
in messageAdapterList
:
243 messageAdapter
.Destroy()
244 self
.eventAdapterDict
[aTopic
].Destroy()
245 del self
.messageAdapterDict
[aTopic
]
246 del self
.eventAdapterDict
[aTopic
]
249 def __getTopics(self
, win
=None):
251 return self
.messageAdapterDict
.keys()
254 return self
.windowTopicLookup
[win
]
256 return self
.EMPTY_LIST
259 def __isDeadWxObject(self
, anObject
):
260 return isinstance(anObject
, wx
._wxPyDeadObject
)
263 def __isDeadTopic(self
, aTopic
):
264 return self
.__isDeadWxObject
(aTopic
[1])
267 def __haveMessageAdapter(self
, eventHandler
, topicPattern
):
269 Return True if there's already a message adapter
273 return self
.messageAdapterDict
[topicPattern
].has_key(eventHandler
)
278 def _determineWindow(self
, aComponent
):
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.
287 if isinstance(aComponent
, wx
.wxMenuItem
):
288 return aComponent
.GetMenu()
294 #---------------------------------------------------------------------------
295 # From here down is implementaion and support classes, although you may
296 # find some of them useful in other contexts.
297 #---------------------------------------------------------------------------
300 class EventMacroInfo
:
302 A class that provides information about event macros.
305 self
.lookupTable
= {}
308 def getEventTypes(self
, eventMacro
):
310 Return the list of event types that the given
311 macro corresponds to.
314 return self
.lookupTable
[eventMacro
]
318 eventMacro(win
, None, None)
320 eventMacro(win
, None)
321 self
.lookupTable
[eventMacro
] = win
.eventTypes
322 return win
.eventTypes
325 def eventIsA(self
, event
, macroList
):
327 Return True if the event is one of the given
330 eventType
= event
.GetEventType()
331 for macro
in macroList
:
332 if eventType
in self
.getEventTypes(macro
):
337 def macroIsA(self
, macro
, macroList
):
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.
344 if callable(macroList
):
345 macroList
= (macroList
,)
346 testList
= self
.getEventTypes(macro
)
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
:
357 def isMultiEvent(self
, macro
):
359 Return True if the given macro actually causes
360 multiple events to be registered.
362 return len(self
.getEventTypes(macro
)) > 1
365 #---------------------------------------------------------------------------
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
378 def Connect(self
, id1
, id2
, eventType
, handlerFunction
):
379 self
.eventTypes
.append(eventType
)
382 #---------------------------------------------------------------------------
386 A class that adapts incoming wxWindows events to
387 Publish/Subscribe messages.
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.
394 def __init__(self
, func
, win
, id):
396 Instantiate a new adapter. Pre-compute my Publish/Subscribe
397 topic, which is constant, and register with wxWindows.
399 self
.publisher
= pubsub
.Publisher()
400 self
.topic
= ((func
, win
, id),)
403 self
.eventType
= _macroInfo
.getEventTypes(func
)[0]
405 # Register myself with the wxWindows event system
407 func(win
, id, self
.handleEvent
)
410 func(win
, self
.handleEvent
)
414 def disconnect(self
):
415 if self
.callStyle
== 3:
416 return self
.win
.Disconnect(self
.id, -1, self
.eventType
)
418 return self
.win
.Disconnect(-1, -1, self
.eventType
)
421 def handleEvent(self
, event
):
423 In response to a wxWindows event, send a PS message
425 self
.publisher
.sendMessage(topic
=self
.topic
, data
=event
)
430 if not self
.disconnect():
431 print 'disconnect failed'
432 except wx
.wxPyDeadObjectError
:
433 print 'disconnect failed: dead object' ##????
436 #---------------------------------------------------------------------------
438 class MessageAdapter
:
440 A class that adapts incoming Publish/Subscribe messages
441 to wxWindows event calls.
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
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.
453 def __init__(self
, eventHandler
, topicPattern
):
455 Instantiate a new MessageAdapter that send wxEvents to the
458 self
.eventHandler
= eventHandler
459 pubsub
.Publisher().subscribe(listener
=self
.deliverEvent
, topic
=(topicPattern
,))
461 def deliverEvent(self
, message
):
462 event
= message
.data
# Extract the wxEvent
463 self
.eventHandler(event
) # Perform the call as wxWindows would
466 pubsub
.Publisher().unsubscribe(listener
=self
.deliverEvent
)
469 #---------------------------------------------------------------------------
472 _macroInfo
= EventMacroInfo()
474 # For now a singleton is not enforced. Should it be or can we trust
476 eventManager
= EventManager()
479 #---------------------------------------------------------------------------
483 if __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
)
494 # Demonstrate 1) register/deregister, 2) Multiple listeners receiving
495 # one event, and 3) Multiple events going to one listener.
498 def printEvent(event
):
499 print 'Name:',event
.GetClassName(),'Timestamp',event
.GetTimestamp()
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
)
508 print '\nDisabling mouse events...'
509 eventManager
.DeregisterWindow(frame
)
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
)
516 frame
.CenterOnScreen()