Coverage for multicall.py: 19%

270 statements  

« 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. 

9 

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. 

15 

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. 

24 

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 

33 

34import tkinter 

35 

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 

45 

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) 

53 

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]]) 

58 

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" 

64 

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. 

72 

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 

81 

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) 

97 

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 

103 

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 

112 

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] 

123 

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 

141 

142_state_subsets = expand_substates(_states) 

143 

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) 

152 

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. 

160 

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 

189 

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))) 

212 

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) 

230 

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) 

237 

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 

245 

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) 

256 

257# which binder should be used for every event type? 

258_binder_classes = (_ComplexBinder,) * 4 + (_SimpleBinder,) * (len(_types)-4) 

259 

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]]) 

264 

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 

296 

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 

303 

304 return modifiers, type, detail 

305 

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]+'>' 

312 

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] 

321 

322 class MultiCall (widget): 

323 assert issubclass(widget, tkinter.Misc) 

324 

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))] 

333 

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) 

351 

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) 

362 

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, []] 

368 

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) 

379 

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) 

393 

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) 

401 

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 

412 

413 _multicall_dict[widget] = MultiCall 

414 return MultiCall 

415 

416 

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>") 

442 

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) 

446 

447 from idlelib.idle_test.htest import run 

448 run(_multi_call)