Coverage for multicall.py: 19%
270 statements
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-11 13:22 -0700
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-11 13:22 -0700
1"""
2MultiCall - a class which inherits its methods from a Tkinter widget (Text, for
3example), but enables multiple calls of functions per virtual event - all
4matching events will be called, not only the most specific one. This is done
5by wrapping the event functions - event_add, event_delete and event_info.
6MultiCall recognizes only a subset of legal event sequences. Sequences which
7are not recognized are treated by the original Tk handling mechanism. A
8more-specific event will be called before a less-specific event.
10The recognized sequences are complete one-event sequences (no emacs-style
11Ctrl-X Ctrl-C, no shortcuts like <3>), for all types of events.
12Key/Button Press/Release events can have modifiers.
13The recognized modifiers are Shift, Control, Option and Command for Mac, and
14Control, Alt, Shift, Meta/M for other platforms.
16For all events which were handled by MultiCall, a new member is added to the
17event instance passed to the binded functions - mc_type. This is one of the
18event type constants defined in this module (such as MC_KEYPRESS).
19For Key/Button events (which are handled by MultiCall and may receive
20modifiers), another member is added - mc_state. This member gives the state
21of the recognized modifiers, as a combination of the modifier constants
22also defined in this module (for example, MC_SHIFT).
23Using these members is absolutely portable.
25The order by which events are called is defined by these rules:
261. A more-specific event will be called before a less-specific event.
272. A recently-binded event will be called before a previously-binded event,
28 unless this conflicts with the first rule.
29Each function will be called at most once for each event.
30"""
31import re
32import sys
34import tkinter
36# the event type constants, which define the meaning of mc_type
37MC_KEYPRESS=0; MC_KEYRELEASE=1; MC_BUTTONPRESS=2; MC_BUTTONRELEASE=3;
38MC_ACTIVATE=4; MC_CIRCULATE=5; MC_COLORMAP=6; MC_CONFIGURE=7;
39MC_DEACTIVATE=8; MC_DESTROY=9; MC_ENTER=10; MC_EXPOSE=11; MC_FOCUSIN=12;
40MC_FOCUSOUT=13; MC_GRAVITY=14; MC_LEAVE=15; MC_MAP=16; MC_MOTION=17;
41MC_MOUSEWHEEL=18; MC_PROPERTY=19; MC_REPARENT=20; MC_UNMAP=21; MC_VISIBILITY=22;
42# the modifier state constants, which define the meaning of mc_state
43MC_SHIFT = 1<<0; MC_CONTROL = 1<<2; MC_ALT = 1<<3; MC_META = 1<<5
44MC_OPTION = 1<<6; MC_COMMAND = 1<<7
46# define the list of modifiers, to be used in complex event types.
47if sys.platform == "darwin": 47 ↛ 51line 47 didn't jump to line 51, because the condition on line 47 was never false
48 _modifiers = (("Shift",), ("Control",), ("Option",), ("Command",))
49 _modifier_masks = (MC_SHIFT, MC_CONTROL, MC_OPTION, MC_COMMAND)
50else:
51 _modifiers = (("Control",), ("Alt",), ("Shift",), ("Meta", "M"))
52 _modifier_masks = (MC_CONTROL, MC_ALT, MC_SHIFT, MC_META)
54# a dictionary to map a modifier name into its number
55_modifier_names = dict([(name, number)
56 for number in range(len(_modifiers))
57 for name in _modifiers[number]])
59# In 3.4, if no shell window is ever open, the underlying Tk widget is
60# destroyed before .__del__ methods here are called. The following
61# is used to selectively ignore shutdown exceptions to avoid
62# 'Exception ignored' messages. See http://bugs.python.org/issue20167
63APPLICATION_GONE = "application has been destroyed"
65# A binder is a class which binds functions to one type of event. It has two
66# methods: bind and unbind, which get a function and a parsed sequence, as
67# returned by _parse_sequence(). There are two types of binders:
68# _SimpleBinder handles event types with no modifiers and no detail.
69# No Python functions are called when no events are binded.
70# _ComplexBinder handles event types with modifiers and a detail.
71# A Python function is called each time an event is generated.
73class _SimpleBinder:
74 def __init__(self, type, widget, widgetinst):
75 self.type = type
76 self.sequence = '<'+_types[type][0]+'>'
77 self.widget = widget
78 self.widgetinst = widgetinst
79 self.bindedfuncs = []
80 self.handlerid = None
82 def bind(self, triplet, func):
83 if not self.handlerid:
84 def handler(event, l = self.bindedfuncs, mc_type = self.type):
85 event.mc_type = mc_type
86 wascalled = {}
87 for i in range(len(l)-1, -1, -1):
88 func = l[i]
89 if func not in wascalled:
90 wascalled[func] = True
91 r = func(event)
92 if r:
93 return r
94 self.handlerid = self.widget.bind(self.widgetinst,
95 self.sequence, handler)
96 self.bindedfuncs.append(func)
98 def unbind(self, triplet, func):
99 self.bindedfuncs.remove(func)
100 if not self.bindedfuncs:
101 self.widget.unbind(self.widgetinst, self.sequence, self.handlerid)
102 self.handlerid = None
104 def __del__(self):
105 if self.handlerid:
106 try:
107 self.widget.unbind(self.widgetinst, self.sequence,
108 self.handlerid)
109 except tkinter.TclError as e:
110 if not APPLICATION_GONE in e.args[0]:
111 raise
113# An int in range(1 << len(_modifiers)) represents a combination of modifiers
114# (if the least significant bit is on, _modifiers[0] is on, and so on).
115# _state_subsets gives for each combination of modifiers, or *state*,
116# a list of the states which are a subset of it. This list is ordered by the
117# number of modifiers is the state - the most specific state comes first.
118_states = range(1 << len(_modifiers))
119_state_names = [''.join(m[0]+'-'
120 for i, m in enumerate(_modifiers)
121 if (1 << i) & s)
122 for s in _states]
124def expand_substates(states):
125 '''For each item of states return a list containing all combinations of
126 that item with individual bits reset, sorted by the number of set bits.
127 '''
128 def nbits(n):
129 "number of bits set in n base 2"
130 nb = 0
131 while n:
132 n, rem = divmod(n, 2)
133 nb += rem
134 return nb
135 statelist = []
136 for state in states:
137 substates = list(set(state & x for x in states))
138 substates.sort(key=nbits, reverse=True)
139 statelist.append(substates)
140 return statelist
142_state_subsets = expand_substates(_states)
144# _state_codes gives for each state, the portable code to be passed as mc_state
145_state_codes = []
146for s in _states:
147 r = 0
148 for i in range(len(_modifiers)):
149 if (1 << i) & s:
150 r |= _modifier_masks[i]
151 _state_codes.append(r)
153class _ComplexBinder:
154 # This class binds many functions, and only unbinds them when it is deleted.
155 # self.handlerids is the list of seqs and ids of binded handler functions.
156 # The binded functions sit in a dictionary of lists of lists, which maps
157 # a detail (or None) and a state into a list of functions.
158 # When a new detail is discovered, handlers for all the possible states
159 # are binded.
161 def __create_handler(self, lists, mc_type, mc_state):
162 def handler(event, lists = lists,
163 mc_type = mc_type, mc_state = mc_state,
164 ishandlerrunning = self.ishandlerrunning,
165 doafterhandler = self.doafterhandler):
166 ishandlerrunning[:] = [True]
167 event.mc_type = mc_type
168 event.mc_state = mc_state
169 wascalled = {}
170 r = None
171 for l in lists:
172 for i in range(len(l)-1, -1, -1):
173 func = l[i]
174 if func not in wascalled:
175 wascalled[func] = True
176 r = l[i](event)
177 if r:
178 break
179 if r:
180 break
181 ishandlerrunning[:] = []
182 # Call all functions in doafterhandler and remove them from list
183 for f in doafterhandler:
184 f()
185 doafterhandler[:] = []
186 if r:
187 return r
188 return handler
190 def __init__(self, type, widget, widgetinst):
191 self.type = type
192 self.typename = _types[type][0]
193 self.widget = widget
194 self.widgetinst = widgetinst
195 self.bindedfuncs = {None: [[] for s in _states]}
196 self.handlerids = []
197 # we don't want to change the lists of functions while a handler is
198 # running - it will mess up the loop and anyway, we usually want the
199 # change to happen from the next event. So we have a list of functions
200 # for the handler to run after it finishes calling the binded functions.
201 # It calls them only once.
202 # ishandlerrunning is a list. An empty one means no, otherwise - yes.
203 # this is done so that it would be mutable.
204 self.ishandlerrunning = []
205 self.doafterhandler = []
206 for s in _states:
207 lists = [self.bindedfuncs[None][i] for i in _state_subsets[s]]
208 handler = self.__create_handler(lists, type, _state_codes[s])
209 seq = '<'+_state_names[s]+self.typename+'>'
210 self.handlerids.append((seq, self.widget.bind(self.widgetinst,
211 seq, handler)))
213 def bind(self, triplet, func):
214 if triplet[2] not in self.bindedfuncs:
215 self.bindedfuncs[triplet[2]] = [[] for s in _states]
216 for s in _states:
217 lists = [ self.bindedfuncs[detail][i]
218 for detail in (triplet[2], None)
219 for i in _state_subsets[s] ]
220 handler = self.__create_handler(lists, self.type,
221 _state_codes[s])
222 seq = "<%s%s-%s>"% (_state_names[s], self.typename, triplet[2])
223 self.handlerids.append((seq, self.widget.bind(self.widgetinst,
224 seq, handler)))
225 doit = lambda: self.bindedfuncs[triplet[2]][triplet[0]].append(func)
226 if not self.ishandlerrunning:
227 doit()
228 else:
229 self.doafterhandler.append(doit)
231 def unbind(self, triplet, func):
232 doit = lambda: self.bindedfuncs[triplet[2]][triplet[0]].remove(func)
233 if not self.ishandlerrunning:
234 doit()
235 else:
236 self.doafterhandler.append(doit)
238 def __del__(self):
239 for seq, id in self.handlerids:
240 try:
241 self.widget.unbind(self.widgetinst, seq, id)
242 except tkinter.TclError as e:
243 if not APPLICATION_GONE in e.args[0]:
244 raise
246# define the list of event types to be handled by MultiEvent. the order is
247# compatible with the definition of event type constants.
248_types = (
249 ("KeyPress", "Key"), ("KeyRelease",), ("ButtonPress", "Button"),
250 ("ButtonRelease",), ("Activate",), ("Circulate",), ("Colormap",),
251 ("Configure",), ("Deactivate",), ("Destroy",), ("Enter",), ("Expose",),
252 ("FocusIn",), ("FocusOut",), ("Gravity",), ("Leave",), ("Map",),
253 ("Motion",), ("MouseWheel",), ("Property",), ("Reparent",), ("Unmap",),
254 ("Visibility",),
255)
257# which binder should be used for every event type?
258_binder_classes = (_ComplexBinder,) * 4 + (_SimpleBinder,) * (len(_types)-4)
260# A dictionary to map a type name into its number
261_type_names = dict([(name, number)
262 for number in range(len(_types))
263 for name in _types[number]])
265_keysym_re = re.compile(r"^\w+$")
266_button_re = re.compile(r"^[1-5]$")
267def _parse_sequence(sequence):
268 """Get a string which should describe an event sequence. If it is
269 successfully parsed as one, return a tuple containing the state (as an int),
270 the event type (as an index of _types), and the detail - None if none, or a
271 string if there is one. If the parsing is unsuccessful, return None.
272 """
273 if not sequence or sequence[0] != '<' or sequence[-1] != '>':
274 return None
275 words = sequence[1:-1].split('-')
276 modifiers = 0
277 while words and words[0] in _modifier_names:
278 modifiers |= 1 << _modifier_names[words[0]]
279 del words[0]
280 if words and words[0] in _type_names:
281 type = _type_names[words[0]]
282 del words[0]
283 else:
284 return None
285 if _binder_classes[type] is _SimpleBinder:
286 if modifiers or words:
287 return None
288 else:
289 detail = None
290 else:
291 # _ComplexBinder
292 if type in [_type_names[s] for s in ("KeyPress", "KeyRelease")]:
293 type_re = _keysym_re
294 else:
295 type_re = _button_re
297 if not words:
298 detail = None
299 elif len(words) == 1 and type_re.match(words[0]):
300 detail = words[0]
301 else:
302 return None
304 return modifiers, type, detail
306def _triplet_to_sequence(triplet):
307 if triplet[2]:
308 return '<'+_state_names[triplet[0]]+_types[triplet[1]][0]+'-'+ \
309 triplet[2]+'>'
310 else:
311 return '<'+_state_names[triplet[0]]+_types[triplet[1]][0]+'>'
313_multicall_dict = {}
314def MultiCallCreator(widget):
315 """Return a MultiCall class which inherits its methods from the
316 given widget class (for example, Tkinter.Text). This is used
317 instead of a templating mechanism.
318 """
319 if widget in _multicall_dict:
320 return _multicall_dict[widget]
322 class MultiCall (widget):
323 assert issubclass(widget, tkinter.Misc)
325 def __init__(self, *args, **kwargs):
326 widget.__init__(self, *args, **kwargs)
327 # a dictionary which maps a virtual event to a tuple with:
328 # 0. the function binded
329 # 1. a list of triplets - the sequences it is binded to
330 self.__eventinfo = {}
331 self.__binders = [_binder_classes[i](i, widget, self)
332 for i in range(len(_types))]
334 def bind(self, sequence=None, func=None, add=None):
335 #print("bind(%s, %s, %s)" % (sequence, func, add),
336 # file=sys.__stderr__)
337 if type(sequence) is str and len(sequence) > 2 and \
338 sequence[:2] == "<<" and sequence[-2:] == ">>":
339 if sequence in self.__eventinfo:
340 ei = self.__eventinfo[sequence]
341 if ei[0] is not None:
342 for triplet in ei[1]:
343 self.__binders[triplet[1]].unbind(triplet, ei[0])
344 ei[0] = func
345 if ei[0] is not None:
346 for triplet in ei[1]:
347 self.__binders[triplet[1]].bind(triplet, func)
348 else:
349 self.__eventinfo[sequence] = [func, []]
350 return widget.bind(self, sequence, func, add)
352 def unbind(self, sequence, funcid=None):
353 if type(sequence) is str and len(sequence) > 2 and \
354 sequence[:2] == "<<" and sequence[-2:] == ">>" and \
355 sequence in self.__eventinfo:
356 func, triplets = self.__eventinfo[sequence]
357 if func is not None:
358 for triplet in triplets:
359 self.__binders[triplet[1]].unbind(triplet, func)
360 self.__eventinfo[sequence][0] = None
361 return widget.unbind(self, sequence, funcid)
363 def event_add(self, virtual, *sequences):
364 #print("event_add(%s, %s)" % (repr(virtual), repr(sequences)),
365 # file=sys.__stderr__)
366 if virtual not in self.__eventinfo:
367 self.__eventinfo[virtual] = [None, []]
369 func, triplets = self.__eventinfo[virtual]
370 for seq in sequences:
371 triplet = _parse_sequence(seq)
372 if triplet is None:
373 #print("Tkinter event_add(%s)" % seq, file=sys.__stderr__)
374 widget.event_add(self, virtual, seq)
375 else:
376 if func is not None:
377 self.__binders[triplet[1]].bind(triplet, func)
378 triplets.append(triplet)
380 def event_delete(self, virtual, *sequences):
381 if virtual not in self.__eventinfo:
382 return
383 func, triplets = self.__eventinfo[virtual]
384 for seq in sequences:
385 triplet = _parse_sequence(seq)
386 if triplet is None:
387 #print("Tkinter event_delete: %s" % seq, file=sys.__stderr__)
388 widget.event_delete(self, virtual, seq)
389 else:
390 if func is not None:
391 self.__binders[triplet[1]].unbind(triplet, func)
392 triplets.remove(triplet)
394 def event_info(self, virtual=None):
395 if virtual is None or virtual not in self.__eventinfo:
396 return widget.event_info(self, virtual)
397 else:
398 return tuple(map(_triplet_to_sequence,
399 self.__eventinfo[virtual][1])) + \
400 widget.event_info(self, virtual)
402 def __del__(self):
403 for virtual in self.__eventinfo:
404 func, triplets = self.__eventinfo[virtual]
405 if func:
406 for triplet in triplets:
407 try:
408 self.__binders[triplet[1]].unbind(triplet, func)
409 except tkinter.TclError as e:
410 if not APPLICATION_GONE in e.args[0]:
411 raise
413 _multicall_dict[widget] = MultiCall
414 return MultiCall
417def _multi_call(parent): # htest #
418 top = tkinter.Toplevel(parent)
419 top.title("Test MultiCall")
420 x, y = map(int, parent.geometry().split('+')[1:])
421 top.geometry("+%d+%d" % (x, y + 175))
422 text = MultiCallCreator(tkinter.Text)(top)
423 text.pack()
424 def bindseq(seq, n=[0]):
425 def handler(event):
426 print(seq)
427 text.bind("<<handler%d>>"%n[0], handler)
428 text.event_add("<<handler%d>>"%n[0], seq)
429 n[0] += 1
430 bindseq("<Key>")
431 bindseq("<Control-Key>")
432 bindseq("<Alt-Key-a>")
433 bindseq("<Control-Key-a>")
434 bindseq("<Alt-Control-Key-a>")
435 bindseq("<Key-b>")
436 bindseq("<Control-Button-1>")
437 bindseq("<Button-2>")
438 bindseq("<Alt-Button-1>")
439 bindseq("<FocusOut>")
440 bindseq("<Enter>")
441 bindseq("<Leave>")
443if __name__ == "__main__": 443 ↛ 444line 443 didn't jump to line 444, because the condition on line 443 was never true
444 from unittest import main
445 main('idlelib.idle_test.test_mainmenu', verbosity=2, exit=False)
447 from idlelib.idle_test.htest import run
448 run(_multi_call)