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