jpayne@68: """ jpayne@68: MultiCall - a class which inherits its methods from a Tkinter widget (Text, for jpayne@68: example), but enables multiple calls of functions per virtual event - all jpayne@68: matching events will be called, not only the most specific one. This is done jpayne@68: by wrapping the event functions - event_add, event_delete and event_info. jpayne@68: MultiCall recognizes only a subset of legal event sequences. Sequences which jpayne@68: are not recognized are treated by the original Tk handling mechanism. A jpayne@68: more-specific event will be called before a less-specific event. jpayne@68: jpayne@68: The recognized sequences are complete one-event sequences (no emacs-style jpayne@68: Ctrl-X Ctrl-C, no shortcuts like <3>), for all types of events. jpayne@68: Key/Button Press/Release events can have modifiers. jpayne@68: The recognized modifiers are Shift, Control, Option and Command for Mac, and jpayne@68: Control, Alt, Shift, Meta/M for other platforms. jpayne@68: jpayne@68: For all events which were handled by MultiCall, a new member is added to the jpayne@68: event instance passed to the binded functions - mc_type. This is one of the jpayne@68: event type constants defined in this module (such as MC_KEYPRESS). jpayne@68: For Key/Button events (which are handled by MultiCall and may receive jpayne@68: modifiers), another member is added - mc_state. This member gives the state jpayne@68: of the recognized modifiers, as a combination of the modifier constants jpayne@68: also defined in this module (for example, MC_SHIFT). jpayne@68: Using these members is absolutely portable. jpayne@68: jpayne@68: The order by which events are called is defined by these rules: jpayne@68: 1. A more-specific event will be called before a less-specific event. jpayne@68: 2. A recently-binded event will be called before a previously-binded event, jpayne@68: unless this conflicts with the first rule. jpayne@68: Each function will be called at most once for each event. jpayne@68: """ jpayne@68: import re jpayne@68: import sys jpayne@68: jpayne@68: import tkinter jpayne@68: jpayne@68: # the event type constants, which define the meaning of mc_type jpayne@68: MC_KEYPRESS=0; MC_KEYRELEASE=1; MC_BUTTONPRESS=2; MC_BUTTONRELEASE=3; jpayne@68: MC_ACTIVATE=4; MC_CIRCULATE=5; MC_COLORMAP=6; MC_CONFIGURE=7; jpayne@68: MC_DEACTIVATE=8; MC_DESTROY=9; MC_ENTER=10; MC_EXPOSE=11; MC_FOCUSIN=12; jpayne@68: MC_FOCUSOUT=13; MC_GRAVITY=14; MC_LEAVE=15; MC_MAP=16; MC_MOTION=17; jpayne@68: MC_MOUSEWHEEL=18; MC_PROPERTY=19; MC_REPARENT=20; MC_UNMAP=21; MC_VISIBILITY=22; jpayne@68: # the modifier state constants, which define the meaning of mc_state jpayne@68: MC_SHIFT = 1<<0; MC_CONTROL = 1<<2; MC_ALT = 1<<3; MC_META = 1<<5 jpayne@68: MC_OPTION = 1<<6; MC_COMMAND = 1<<7 jpayne@68: jpayne@68: # define the list of modifiers, to be used in complex event types. jpayne@68: if sys.platform == "darwin": jpayne@68: _modifiers = (("Shift",), ("Control",), ("Option",), ("Command",)) jpayne@68: _modifier_masks = (MC_SHIFT, MC_CONTROL, MC_OPTION, MC_COMMAND) jpayne@68: else: jpayne@68: _modifiers = (("Control",), ("Alt",), ("Shift",), ("Meta", "M")) jpayne@68: _modifier_masks = (MC_CONTROL, MC_ALT, MC_SHIFT, MC_META) jpayne@68: jpayne@68: # a dictionary to map a modifier name into its number jpayne@68: _modifier_names = dict([(name, number) jpayne@68: for number in range(len(_modifiers)) jpayne@68: for name in _modifiers[number]]) jpayne@68: jpayne@68: # In 3.4, if no shell window is ever open, the underlying Tk widget is jpayne@68: # destroyed before .__del__ methods here are called. The following jpayne@68: # is used to selectively ignore shutdown exceptions to avoid jpayne@68: # 'Exception ignored' messages. See http://bugs.python.org/issue20167 jpayne@68: APPLICATION_GONE = "application has been destroyed" jpayne@68: jpayne@68: # A binder is a class which binds functions to one type of event. It has two jpayne@68: # methods: bind and unbind, which get a function and a parsed sequence, as jpayne@68: # returned by _parse_sequence(). There are two types of binders: jpayne@68: # _SimpleBinder handles event types with no modifiers and no detail. jpayne@68: # No Python functions are called when no events are binded. jpayne@68: # _ComplexBinder handles event types with modifiers and a detail. jpayne@68: # A Python function is called each time an event is generated. jpayne@68: jpayne@68: class _SimpleBinder: jpayne@68: def __init__(self, type, widget, widgetinst): jpayne@68: self.type = type jpayne@68: self.sequence = '<'+_types[type][0]+'>' jpayne@68: self.widget = widget jpayne@68: self.widgetinst = widgetinst jpayne@68: self.bindedfuncs = [] jpayne@68: self.handlerid = None jpayne@68: jpayne@68: def bind(self, triplet, func): jpayne@68: if not self.handlerid: jpayne@68: def handler(event, l = self.bindedfuncs, mc_type = self.type): jpayne@68: event.mc_type = mc_type jpayne@68: wascalled = {} jpayne@68: for i in range(len(l)-1, -1, -1): jpayne@68: func = l[i] jpayne@68: if func not in wascalled: jpayne@68: wascalled[func] = True jpayne@68: r = func(event) jpayne@68: if r: jpayne@68: return r jpayne@68: self.handlerid = self.widget.bind(self.widgetinst, jpayne@68: self.sequence, handler) jpayne@68: self.bindedfuncs.append(func) jpayne@68: jpayne@68: def unbind(self, triplet, func): jpayne@68: self.bindedfuncs.remove(func) jpayne@68: if not self.bindedfuncs: jpayne@68: self.widget.unbind(self.widgetinst, self.sequence, self.handlerid) jpayne@68: self.handlerid = None jpayne@68: jpayne@68: def __del__(self): jpayne@68: if self.handlerid: jpayne@68: try: jpayne@68: self.widget.unbind(self.widgetinst, self.sequence, jpayne@68: self.handlerid) jpayne@68: except tkinter.TclError as e: jpayne@68: if not APPLICATION_GONE in e.args[0]: jpayne@68: raise jpayne@68: jpayne@68: # An int in range(1 << len(_modifiers)) represents a combination of modifiers jpayne@68: # (if the least significant bit is on, _modifiers[0] is on, and so on). jpayne@68: # _state_subsets gives for each combination of modifiers, or *state*, jpayne@68: # a list of the states which are a subset of it. This list is ordered by the jpayne@68: # number of modifiers is the state - the most specific state comes first. jpayne@68: _states = range(1 << len(_modifiers)) jpayne@68: _state_names = [''.join(m[0]+'-' jpayne@68: for i, m in enumerate(_modifiers) jpayne@68: if (1 << i) & s) jpayne@68: for s in _states] jpayne@68: jpayne@68: def expand_substates(states): jpayne@68: '''For each item of states return a list containing all combinations of jpayne@68: that item with individual bits reset, sorted by the number of set bits. jpayne@68: ''' jpayne@68: def nbits(n): jpayne@68: "number of bits set in n base 2" jpayne@68: nb = 0 jpayne@68: while n: jpayne@68: n, rem = divmod(n, 2) jpayne@68: nb += rem jpayne@68: return nb jpayne@68: statelist = [] jpayne@68: for state in states: jpayne@68: substates = list(set(state & x for x in states)) jpayne@68: substates.sort(key=nbits, reverse=True) jpayne@68: statelist.append(substates) jpayne@68: return statelist jpayne@68: jpayne@68: _state_subsets = expand_substates(_states) jpayne@68: jpayne@68: # _state_codes gives for each state, the portable code to be passed as mc_state jpayne@68: _state_codes = [] jpayne@68: for s in _states: jpayne@68: r = 0 jpayne@68: for i in range(len(_modifiers)): jpayne@68: if (1 << i) & s: jpayne@68: r |= _modifier_masks[i] jpayne@68: _state_codes.append(r) jpayne@68: jpayne@68: class _ComplexBinder: jpayne@68: # This class binds many functions, and only unbinds them when it is deleted. jpayne@68: # self.handlerids is the list of seqs and ids of binded handler functions. jpayne@68: # The binded functions sit in a dictionary of lists of lists, which maps jpayne@68: # a detail (or None) and a state into a list of functions. jpayne@68: # When a new detail is discovered, handlers for all the possible states jpayne@68: # are binded. jpayne@68: jpayne@68: def __create_handler(self, lists, mc_type, mc_state): jpayne@68: def handler(event, lists = lists, jpayne@68: mc_type = mc_type, mc_state = mc_state, jpayne@68: ishandlerrunning = self.ishandlerrunning, jpayne@68: doafterhandler = self.doafterhandler): jpayne@68: ishandlerrunning[:] = [True] jpayne@68: event.mc_type = mc_type jpayne@68: event.mc_state = mc_state jpayne@68: wascalled = {} jpayne@68: r = None jpayne@68: for l in lists: jpayne@68: for i in range(len(l)-1, -1, -1): jpayne@68: func = l[i] jpayne@68: if func not in wascalled: jpayne@68: wascalled[func] = True jpayne@68: r = l[i](event) jpayne@68: if r: jpayne@68: break jpayne@68: if r: jpayne@68: break jpayne@68: ishandlerrunning[:] = [] jpayne@68: # Call all functions in doafterhandler and remove them from list jpayne@68: for f in doafterhandler: jpayne@68: f() jpayne@68: doafterhandler[:] = [] jpayne@68: if r: jpayne@68: return r jpayne@68: return handler jpayne@68: jpayne@68: def __init__(self, type, widget, widgetinst): jpayne@68: self.type = type jpayne@68: self.typename = _types[type][0] jpayne@68: self.widget = widget jpayne@68: self.widgetinst = widgetinst jpayne@68: self.bindedfuncs = {None: [[] for s in _states]} jpayne@68: self.handlerids = [] jpayne@68: # we don't want to change the lists of functions while a handler is jpayne@68: # running - it will mess up the loop and anyway, we usually want the jpayne@68: # change to happen from the next event. So we have a list of functions jpayne@68: # for the handler to run after it finishes calling the binded functions. jpayne@68: # It calls them only once. jpayne@68: # ishandlerrunning is a list. An empty one means no, otherwise - yes. jpayne@68: # this is done so that it would be mutable. jpayne@68: self.ishandlerrunning = [] jpayne@68: self.doafterhandler = [] jpayne@68: for s in _states: jpayne@68: lists = [self.bindedfuncs[None][i] for i in _state_subsets[s]] jpayne@68: handler = self.__create_handler(lists, type, _state_codes[s]) jpayne@68: seq = '<'+_state_names[s]+self.typename+'>' jpayne@68: self.handlerids.append((seq, self.widget.bind(self.widgetinst, jpayne@68: seq, handler))) jpayne@68: jpayne@68: def bind(self, triplet, func): jpayne@68: if triplet[2] not in self.bindedfuncs: jpayne@68: self.bindedfuncs[triplet[2]] = [[] for s in _states] jpayne@68: for s in _states: jpayne@68: lists = [ self.bindedfuncs[detail][i] jpayne@68: for detail in (triplet[2], None) jpayne@68: for i in _state_subsets[s] ] jpayne@68: handler = self.__create_handler(lists, self.type, jpayne@68: _state_codes[s]) jpayne@68: seq = "<%s%s-%s>"% (_state_names[s], self.typename, triplet[2]) jpayne@68: self.handlerids.append((seq, self.widget.bind(self.widgetinst, jpayne@68: seq, handler))) jpayne@68: doit = lambda: self.bindedfuncs[triplet[2]][triplet[0]].append(func) jpayne@68: if not self.ishandlerrunning: jpayne@68: doit() jpayne@68: else: jpayne@68: self.doafterhandler.append(doit) jpayne@68: jpayne@68: def unbind(self, triplet, func): jpayne@68: doit = lambda: self.bindedfuncs[triplet[2]][triplet[0]].remove(func) jpayne@68: if not self.ishandlerrunning: jpayne@68: doit() jpayne@68: else: jpayne@68: self.doafterhandler.append(doit) jpayne@68: jpayne@68: def __del__(self): jpayne@68: for seq, id in self.handlerids: jpayne@68: try: jpayne@68: self.widget.unbind(self.widgetinst, seq, id) jpayne@68: except tkinter.TclError as e: jpayne@68: if not APPLICATION_GONE in e.args[0]: jpayne@68: raise jpayne@68: jpayne@68: # define the list of event types to be handled by MultiEvent. the order is jpayne@68: # compatible with the definition of event type constants. jpayne@68: _types = ( jpayne@68: ("KeyPress", "Key"), ("KeyRelease",), ("ButtonPress", "Button"), jpayne@68: ("ButtonRelease",), ("Activate",), ("Circulate",), ("Colormap",), jpayne@68: ("Configure",), ("Deactivate",), ("Destroy",), ("Enter",), ("Expose",), jpayne@68: ("FocusIn",), ("FocusOut",), ("Gravity",), ("Leave",), ("Map",), jpayne@68: ("Motion",), ("MouseWheel",), ("Property",), ("Reparent",), ("Unmap",), jpayne@68: ("Visibility",), jpayne@68: ) jpayne@68: jpayne@68: # which binder should be used for every event type? jpayne@68: _binder_classes = (_ComplexBinder,) * 4 + (_SimpleBinder,) * (len(_types)-4) jpayne@68: jpayne@68: # A dictionary to map a type name into its number jpayne@68: _type_names = dict([(name, number) jpayne@68: for number in range(len(_types)) jpayne@68: for name in _types[number]]) jpayne@68: jpayne@68: _keysym_re = re.compile(r"^\w+$") jpayne@68: _button_re = re.compile(r"^[1-5]$") jpayne@68: def _parse_sequence(sequence): jpayne@68: """Get a string which should describe an event sequence. If it is jpayne@68: successfully parsed as one, return a tuple containing the state (as an int), jpayne@68: the event type (as an index of _types), and the detail - None if none, or a jpayne@68: string if there is one. If the parsing is unsuccessful, return None. jpayne@68: """ jpayne@68: if not sequence or sequence[0] != '<' or sequence[-1] != '>': jpayne@68: return None jpayne@68: words = sequence[1:-1].split('-') jpayne@68: modifiers = 0 jpayne@68: while words and words[0] in _modifier_names: jpayne@68: modifiers |= 1 << _modifier_names[words[0]] jpayne@68: del words[0] jpayne@68: if words and words[0] in _type_names: jpayne@68: type = _type_names[words[0]] jpayne@68: del words[0] jpayne@68: else: jpayne@68: return None jpayne@68: if _binder_classes[type] is _SimpleBinder: jpayne@68: if modifiers or words: jpayne@68: return None jpayne@68: else: jpayne@68: detail = None jpayne@68: else: jpayne@68: # _ComplexBinder jpayne@68: if type in [_type_names[s] for s in ("KeyPress", "KeyRelease")]: jpayne@68: type_re = _keysym_re jpayne@68: else: jpayne@68: type_re = _button_re jpayne@68: jpayne@68: if not words: jpayne@68: detail = None jpayne@68: elif len(words) == 1 and type_re.match(words[0]): jpayne@68: detail = words[0] jpayne@68: else: jpayne@68: return None jpayne@68: jpayne@68: return modifiers, type, detail jpayne@68: jpayne@68: def _triplet_to_sequence(triplet): jpayne@68: if triplet[2]: jpayne@68: return '<'+_state_names[triplet[0]]+_types[triplet[1]][0]+'-'+ \ jpayne@68: triplet[2]+'>' jpayne@68: else: jpayne@68: return '<'+_state_names[triplet[0]]+_types[triplet[1]][0]+'>' jpayne@68: jpayne@68: _multicall_dict = {} jpayne@68: def MultiCallCreator(widget): jpayne@68: """Return a MultiCall class which inherits its methods from the jpayne@68: given widget class (for example, Tkinter.Text). This is used jpayne@68: instead of a templating mechanism. jpayne@68: """ jpayne@68: if widget in _multicall_dict: jpayne@68: return _multicall_dict[widget] jpayne@68: jpayne@68: class MultiCall (widget): jpayne@68: assert issubclass(widget, tkinter.Misc) jpayne@68: jpayne@68: def __init__(self, *args, **kwargs): jpayne@68: widget.__init__(self, *args, **kwargs) jpayne@68: # a dictionary which maps a virtual event to a tuple with: jpayne@68: # 0. the function binded jpayne@68: # 1. a list of triplets - the sequences it is binded to jpayne@68: self.__eventinfo = {} jpayne@68: self.__binders = [_binder_classes[i](i, widget, self) jpayne@68: for i in range(len(_types))] jpayne@68: jpayne@68: def bind(self, sequence=None, func=None, add=None): jpayne@68: #print("bind(%s, %s, %s)" % (sequence, func, add), jpayne@68: # file=sys.__stderr__) jpayne@68: if type(sequence) is str and len(sequence) > 2 and \ jpayne@68: sequence[:2] == "<<" and sequence[-2:] == ">>": jpayne@68: if sequence in self.__eventinfo: jpayne@68: ei = self.__eventinfo[sequence] jpayne@68: if ei[0] is not None: jpayne@68: for triplet in ei[1]: jpayne@68: self.__binders[triplet[1]].unbind(triplet, ei[0]) jpayne@68: ei[0] = func jpayne@68: if ei[0] is not None: jpayne@68: for triplet in ei[1]: jpayne@68: self.__binders[triplet[1]].bind(triplet, func) jpayne@68: else: jpayne@68: self.__eventinfo[sequence] = [func, []] jpayne@68: return widget.bind(self, sequence, func, add) jpayne@68: jpayne@68: def unbind(self, sequence, funcid=None): jpayne@68: if type(sequence) is str and len(sequence) > 2 and \ jpayne@68: sequence[:2] == "<<" and sequence[-2:] == ">>" and \ jpayne@68: sequence in self.__eventinfo: jpayne@68: func, triplets = self.__eventinfo[sequence] jpayne@68: if func is not None: jpayne@68: for triplet in triplets: jpayne@68: self.__binders[triplet[1]].unbind(triplet, func) jpayne@68: self.__eventinfo[sequence][0] = None jpayne@68: return widget.unbind(self, sequence, funcid) jpayne@68: jpayne@68: def event_add(self, virtual, *sequences): jpayne@68: #print("event_add(%s, %s)" % (repr(virtual), repr(sequences)), jpayne@68: # file=sys.__stderr__) jpayne@68: if virtual not in self.__eventinfo: jpayne@68: self.__eventinfo[virtual] = [None, []] jpayne@68: jpayne@68: func, triplets = self.__eventinfo[virtual] jpayne@68: for seq in sequences: jpayne@68: triplet = _parse_sequence(seq) jpayne@68: if triplet is None: jpayne@68: #print("Tkinter event_add(%s)" % seq, file=sys.__stderr__) jpayne@68: widget.event_add(self, virtual, seq) jpayne@68: else: jpayne@68: if func is not None: jpayne@68: self.__binders[triplet[1]].bind(triplet, func) jpayne@68: triplets.append(triplet) jpayne@68: jpayne@68: def event_delete(self, virtual, *sequences): jpayne@68: if virtual not in self.__eventinfo: jpayne@68: return jpayne@68: func, triplets = self.__eventinfo[virtual] jpayne@68: for seq in sequences: jpayne@68: triplet = _parse_sequence(seq) jpayne@68: if triplet is None: jpayne@68: #print("Tkinter event_delete: %s" % seq, file=sys.__stderr__) jpayne@68: widget.event_delete(self, virtual, seq) jpayne@68: else: jpayne@68: if func is not None: jpayne@68: self.__binders[triplet[1]].unbind(triplet, func) jpayne@68: triplets.remove(triplet) jpayne@68: jpayne@68: def event_info(self, virtual=None): jpayne@68: if virtual is None or virtual not in self.__eventinfo: jpayne@68: return widget.event_info(self, virtual) jpayne@68: else: jpayne@68: return tuple(map(_triplet_to_sequence, jpayne@68: self.__eventinfo[virtual][1])) + \ jpayne@68: widget.event_info(self, virtual) jpayne@68: jpayne@68: def __del__(self): jpayne@68: for virtual in self.__eventinfo: jpayne@68: func, triplets = self.__eventinfo[virtual] jpayne@68: if func: jpayne@68: for triplet in triplets: jpayne@68: try: jpayne@68: self.__binders[triplet[1]].unbind(triplet, func) jpayne@68: except tkinter.TclError as e: jpayne@68: if not APPLICATION_GONE in e.args[0]: jpayne@68: raise jpayne@68: jpayne@68: _multicall_dict[widget] = MultiCall jpayne@68: return MultiCall jpayne@68: jpayne@68: jpayne@68: def _multi_call(parent): # htest # jpayne@68: top = tkinter.Toplevel(parent) jpayne@68: top.title("Test MultiCall") jpayne@68: x, y = map(int, parent.geometry().split('+')[1:]) jpayne@68: top.geometry("+%d+%d" % (x, y + 175)) jpayne@68: text = MultiCallCreator(tkinter.Text)(top) jpayne@68: text.pack() jpayne@68: def bindseq(seq, n=[0]): jpayne@68: def handler(event): jpayne@68: print(seq) jpayne@68: text.bind("<>"%n[0], handler) jpayne@68: text.event_add("<>"%n[0], seq) jpayne@68: n[0] += 1 jpayne@68: bindseq("") jpayne@68: bindseq("") jpayne@68: bindseq("") jpayne@68: bindseq("") jpayne@68: bindseq("") jpayne@68: bindseq("") jpayne@68: bindseq("") jpayne@68: bindseq("") jpayne@68: bindseq("") jpayne@68: bindseq("") jpayne@68: bindseq("") jpayne@68: bindseq("") jpayne@68: jpayne@68: if __name__ == "__main__": jpayne@68: from unittest import main jpayne@68: main('idlelib.idle_test.test_mainmenu', verbosity=2, exit=False) jpayne@68: jpayne@68: from idlelib.idle_test.htest import run jpayne@68: run(_multi_call)