Home | History | Annotate | Download | only in idlelib
      1 from __future__ import print_function
      2 from Tkinter import TclError
      3 
      4 class WidgetRedirector:
      5     """Support for redirecting arbitrary widget subcommands.
      6 
      7     Some Tk operations don't normally pass through tkinter.  For example, if a
      8     character is inserted into a Text widget by pressing a key, a default Tk
      9     binding to the widget's 'insert' operation is activated, and the Tk library
     10     processes the insert without calling back into tkinter.
     11 
     12     Although a binding to <Key> could be made via tkinter, what we really want
     13     to do is to hook the Tk 'insert' operation itself.  For one thing, we want
     14     a text.insert call in idle code to have the same effect as a key press.
     15 
     16     When a widget is instantiated, a Tcl command is created whose name is the
     17     same as the pathname widget._w.  This command is used to invoke the various
     18     widget operations, e.g. insert (for a Text widget). We are going to hook
     19     this command and provide a facility ('register') to intercept the widget
     20     operation.  We will also intercept method calls on the Tkinter class
     21     instance that represents the tk widget.
     22 
     23     In IDLE, WidgetRedirector is used in Percolator to intercept Text
     24     commands.  The function being registered provides access to the top
     25     of a Percolator chain.  At the bottom of the chain is a call to the
     26     original Tk widget operation.
     27     """
     28     def __init__(self, widget):
     29         '''Initialize attributes and setup redirection.
     30 
     31         _operations: dict mapping operation name to new function.
     32         widget: the widget whose tcl command is to be intercepted.
     33         tk: widget.tk, a convenience attribute, probably not needed.
     34         orig: new name of the original tcl command.
     35 
     36         Since renaming to orig fails with TclError when orig already
     37         exists, only one WidgetDirector can exist for a given widget.
     38         '''
     39         self._operations = {}
     40         self.widget = widget            # widget instance
     41         self.tk = tk = widget.tk        # widget's root
     42         w = widget._w                   # widget's (full) Tk pathname
     43         self.orig = w + "_orig"
     44         # Rename the Tcl command within Tcl:
     45         tk.call("rename", w, self.orig)
     46         # Create a new Tcl command whose name is the widget's pathname, and
     47         # whose action is to dispatch on the operation passed to the widget:
     48         tk.createcommand(w, self.dispatch)
     49 
     50     def __repr__(self):
     51         return "WidgetRedirector(%s<%s>)" % (self.widget.__class__.__name__,
     52                                              self.widget._w)
     53 
     54     def close(self):
     55         "Unregister operations and revert redirection created by .__init__."
     56         for operation in list(self._operations):
     57             self.unregister(operation)
     58         widget = self.widget
     59         tk = widget.tk
     60         w = widget._w
     61         # Restore the original widget Tcl command.
     62         tk.deletecommand(w)
     63         tk.call("rename", self.orig, w)
     64         del self.widget, self.tk  # Should not be needed
     65         # if instance is deleted after close, as in Percolator.
     66 
     67     def register(self, operation, function):
     68         '''Return OriginalCommand(operation) after registering function.
     69 
     70         Registration adds an operation: function pair to ._operations.
     71         It also adds a widget function attribute that masks the Tkinter
     72         class instance method.  Method masking operates independently
     73         from command dispatch.
     74 
     75         If a second function is registered for the same operation, the
     76         first function is replaced in both places.
     77         '''
     78         self._operations[operation] = function
     79         setattr(self.widget, operation, function)
     80         return OriginalCommand(self, operation)
     81 
     82     def unregister(self, operation):
     83         '''Return the function for the operation, or None.
     84 
     85         Deleting the instance attribute unmasks the class attribute.
     86         '''
     87         if operation in self._operations:
     88             function = self._operations[operation]
     89             del self._operations[operation]
     90             try:
     91                 delattr(self.widget, operation)
     92             except AttributeError:
     93                 pass
     94             return function
     95         else:
     96             return None
     97 
     98     def dispatch(self, operation, *args):
     99         '''Callback from Tcl which runs when the widget is referenced.
    100 
    101         If an operation has been registered in self._operations, apply the
    102         associated function to the args passed into Tcl. Otherwise, pass the
    103         operation through to Tk via the original Tcl function.
    104 
    105         Note that if a registered function is called, the operation is not
    106         passed through to Tk.  Apply the function returned by self.register()
    107         to *args to accomplish that.  For an example, see ColorDelegator.py.
    108 
    109         '''
    110         m = self._operations.get(operation)
    111         try:
    112             if m:
    113                 return m(*args)
    114             else:
    115                 return self.tk.call((self.orig, operation) + args)
    116         except TclError:
    117             return ""
    118 
    119 
    120 class OriginalCommand:
    121     '''Callable for original tk command that has been redirected.
    122 
    123     Returned by .register; can be used in the function registered.
    124     redir = WidgetRedirector(text)
    125     def my_insert(*args):
    126         print("insert", args)
    127         original_insert(*args)
    128     original_insert = redir.register("insert", my_insert)
    129     '''
    130 
    131     def __init__(self, redir, operation):
    132         '''Create .tk_call and .orig_and_operation for .__call__ method.
    133 
    134         .redir and .operation store the input args for __repr__.
    135         .tk and .orig copy attributes of .redir (probably not needed).
    136         '''
    137         self.redir = redir
    138         self.operation = operation
    139         self.tk = redir.tk  # redundant with self.redir
    140         self.orig = redir.orig  # redundant with self.redir
    141         # These two could be deleted after checking recipient code.
    142         self.tk_call = redir.tk.call
    143         self.orig_and_operation = (redir.orig, operation)
    144 
    145     def __repr__(self):
    146         return "OriginalCommand(%r, %r)" % (self.redir, self.operation)
    147 
    148     def __call__(self, *args):
    149         return self.tk_call(self.orig_and_operation + args)
    150 
    151 
    152 def _widget_redirector(parent):  # htest #
    153     from Tkinter import Tk, Text
    154     import re
    155 
    156     root = Tk()
    157     root.title("Test WidgetRedirector")
    158     width, height, x, y = list(map(int, re.split('[x+]', parent.geometry())))
    159     root.geometry("+%d+%d"%(x, y + 150))
    160     text = Text(root)
    161     text.pack()
    162     text.focus_set()
    163     redir = WidgetRedirector(text)
    164     def my_insert(*args):
    165         print("insert", args)
    166         original_insert(*args)
    167     original_insert = redir.register("insert", my_insert)
    168     root.mainloop()
    169 
    170 if __name__ == "__main__":
    171     import unittest
    172     unittest.main('idlelib.idle_test.test_widgetredir',
    173                   verbosity=2, exit=False)
    174     from idlelib.idle_test.htest import run
    175     run(_widget_redirector)
    176