Terry Jan Reedy | 7cca28f | 2014-07-10 01:16:49 -0400 | [diff] [blame] | 1 | from tkinter import TclError |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 2 | |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 3 | class WidgetRedirector: |
Guido van Rossum | 8ce8a78 | 2007-11-01 19:42:39 +0000 | [diff] [blame] | 4 | """Support for redirecting arbitrary widget subcommands. |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 5 | |
Terry Jan Reedy | 223dd8d | 2014-07-11 00:16:00 -0400 | [diff] [blame] | 6 | Some Tk operations don't normally pass through tkinter. For example, if a |
Guido van Rossum | 8ce8a78 | 2007-11-01 19:42:39 +0000 | [diff] [blame] | 7 | character is inserted into a Text widget by pressing a key, a default Tk |
| 8 | binding to the widget's 'insert' operation is activated, and the Tk library |
Terry Jan Reedy | 223dd8d | 2014-07-11 00:16:00 -0400 | [diff] [blame] | 9 | processes the insert without calling back into tkinter. |
Guido van Rossum | 8ce8a78 | 2007-11-01 19:42:39 +0000 | [diff] [blame] | 10 | |
Terry Jan Reedy | 223dd8d | 2014-07-11 00:16:00 -0400 | [diff] [blame] | 11 | Although a binding to <Key> could be made via tkinter, what we really want |
| 12 | to do is to hook the Tk 'insert' operation itself. For one thing, we want |
| 13 | a text.insert call in idle code to have the same effect as a key press. |
Guido van Rossum | 8ce8a78 | 2007-11-01 19:42:39 +0000 | [diff] [blame] | 14 | |
| 15 | When a widget is instantiated, a Tcl command is created whose name is the |
| 16 | same as the pathname widget._w. This command is used to invoke the various |
| 17 | widget operations, e.g. insert (for a Text widget). We are going to hook |
| 18 | this command and provide a facility ('register') to intercept the widget |
Terry Jan Reedy | 223dd8d | 2014-07-11 00:16:00 -0400 | [diff] [blame] | 19 | operation. We will also intercept method calls on the tkinter class |
| 20 | instance that represents the tk widget. |
Guido van Rossum | 8ce8a78 | 2007-11-01 19:42:39 +0000 | [diff] [blame] | 21 | |
Terry Jan Reedy | 7cca28f | 2014-07-10 01:16:49 -0400 | [diff] [blame] | 22 | In IDLE, WidgetRedirector is used in Percolator to intercept Text |
| 23 | commands. The function being registered provides access to the top |
| 24 | of a Percolator chain. At the bottom of the chain is a call to the |
| 25 | original Tk widget operation. |
Guido van Rossum | 8ce8a78 | 2007-11-01 19:42:39 +0000 | [diff] [blame] | 26 | """ |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 27 | def __init__(self, widget): |
Terry Jan Reedy | 7cca28f | 2014-07-10 01:16:49 -0400 | [diff] [blame] | 28 | '''Initialize attributes and setup redirection. |
| 29 | |
| 30 | _operations: dict mapping operation name to new function. |
| 31 | widget: the widget whose tcl command is to be intercepted. |
| 32 | tk: widget.tk, a convenience attribute, probably not needed. |
| 33 | orig: new name of the original tcl command. |
| 34 | |
| 35 | Since renaming to orig fails with TclError when orig already |
| 36 | exists, only one WidgetDirector can exist for a given widget. |
| 37 | ''' |
Guido van Rossum | 8ce8a78 | 2007-11-01 19:42:39 +0000 | [diff] [blame] | 38 | self._operations = {} |
| 39 | self.widget = widget # widget instance |
| 40 | self.tk = tk = widget.tk # widget's root |
| 41 | w = widget._w # widget's (full) Tk pathname |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 42 | self.orig = w + "_orig" |
Guido van Rossum | 8ce8a78 | 2007-11-01 19:42:39 +0000 | [diff] [blame] | 43 | # Rename the Tcl command within Tcl: |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 44 | tk.call("rename", w, self.orig) |
Guido van Rossum | 8ce8a78 | 2007-11-01 19:42:39 +0000 | [diff] [blame] | 45 | # Create a new Tcl command whose name is the widget's pathname, and |
| 46 | # whose action is to dispatch on the operation passed to the widget: |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 47 | tk.createcommand(w, self.dispatch) |
| 48 | |
| 49 | def __repr__(self): |
Serhiy Storchaka | 465e60e | 2014-07-25 23:36:00 +0300 | [diff] [blame] | 50 | return "%s(%s<%s>)" % (self.__class__.__name__, |
| 51 | self.widget.__class__.__name__, |
| 52 | self.widget._w) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 53 | |
| 54 | def close(self): |
Terry Jan Reedy | 7cca28f | 2014-07-10 01:16:49 -0400 | [diff] [blame] | 55 | "Unregister operations and revert redirection created by .__init__." |
Guido van Rossum | 8ce8a78 | 2007-11-01 19:42:39 +0000 | [diff] [blame] | 56 | for operation in list(self._operations): |
| 57 | self.unregister(operation) |
Terry Jan Reedy | 7cca28f | 2014-07-10 01:16:49 -0400 | [diff] [blame] | 58 | widget = self.widget |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 59 | tk = widget.tk |
| 60 | w = widget._w |
Terry Jan Reedy | 7cca28f | 2014-07-10 01:16:49 -0400 | [diff] [blame] | 61 | # Restore the original widget Tcl command. |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 62 | tk.deletecommand(w) |
Terry Jan Reedy | 7cca28f | 2014-07-10 01:16:49 -0400 | [diff] [blame] | 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. |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 66 | |
Guido van Rossum | 8ce8a78 | 2007-11-01 19:42:39 +0000 | [diff] [blame] | 67 | def register(self, operation, function): |
Terry Jan Reedy | 7cca28f | 2014-07-10 01:16:49 -0400 | [diff] [blame] | 68 | '''Return OriginalCommand(operation) after registering function. |
| 69 | |
Terry Jan Reedy | 223dd8d | 2014-07-11 00:16:00 -0400 | [diff] [blame] | 70 | Registration adds an operation: function pair to ._operations. |
Serhiy Storchaka | 6a7b3a7 | 2016-04-17 08:32:47 +0300 | [diff] [blame] | 71 | It also adds a widget function attribute that masks the tkinter |
Terry Jan Reedy | 223dd8d | 2014-07-11 00:16:00 -0400 | [diff] [blame] | 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. |
Terry Jan Reedy | 7cca28f | 2014-07-10 01:16:49 -0400 | [diff] [blame] | 77 | ''' |
Guido van Rossum | 8ce8a78 | 2007-11-01 19:42:39 +0000 | [diff] [blame] | 78 | self._operations[operation] = function |
| 79 | setattr(self.widget, operation, function) |
| 80 | return OriginalCommand(self, operation) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 81 | |
Guido van Rossum | 8ce8a78 | 2007-11-01 19:42:39 +0000 | [diff] [blame] | 82 | def unregister(self, operation): |
Terry Jan Reedy | 7cca28f | 2014-07-10 01:16:49 -0400 | [diff] [blame] | 83 | '''Return the function for the operation, or None. |
| 84 | |
| 85 | Deleting the instance attribute unmasks the class attribute. |
| 86 | ''' |
Guido van Rossum | 8ce8a78 | 2007-11-01 19:42:39 +0000 | [diff] [blame] | 87 | if operation in self._operations: |
| 88 | function = self._operations[operation] |
| 89 | del self._operations[operation] |
Terry Jan Reedy | 223dd8d | 2014-07-11 00:16:00 -0400 | [diff] [blame] | 90 | try: |
Guido van Rossum | 8ce8a78 | 2007-11-01 19:42:39 +0000 | [diff] [blame] | 91 | delattr(self.widget, operation) |
Terry Jan Reedy | 223dd8d | 2014-07-11 00:16:00 -0400 | [diff] [blame] | 92 | except AttributeError: |
| 93 | pass |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 94 | return function |
| 95 | else: |
| 96 | return None |
| 97 | |
Guido van Rossum | 8ce8a78 | 2007-11-01 19:42:39 +0000 | [diff] [blame] | 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() |
Terry Jan Reedy | 6fa5bdc | 2016-05-28 13:22:31 -0400 | [diff] [blame] | 107 | to *args to accomplish that. For an example, see colorizer.py. |
Guido van Rossum | 8ce8a78 | 2007-11-01 19:42:39 +0000 | [diff] [blame] | 108 | |
| 109 | ''' |
| 110 | m = self._operations.get(operation) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 111 | try: |
| 112 | if m: |
Raymond Hettinger | 931237e | 2003-07-09 18:48:24 +0000 | [diff] [blame] | 113 | return m(*args) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 114 | else: |
Guido van Rossum | 8ce8a78 | 2007-11-01 19:42:39 +0000 | [diff] [blame] | 115 | return self.tk.call((self.orig, operation) + args) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 116 | except TclError: |
| 117 | return "" |
| 118 | |
| 119 | |
| 120 | class OriginalCommand: |
Terry Jan Reedy | 7cca28f | 2014-07-10 01:16:49 -0400 | [diff] [blame] | 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 | ''' |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 130 | |
Guido van Rossum | 8ce8a78 | 2007-11-01 19:42:39 +0000 | [diff] [blame] | 131 | def __init__(self, redir, operation): |
Terry Jan Reedy | 7cca28f | 2014-07-10 01:16:49 -0400 | [diff] [blame] | 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 | ''' |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 137 | self.redir = redir |
Guido van Rossum | 8ce8a78 | 2007-11-01 19:42:39 +0000 | [diff] [blame] | 138 | self.operation = operation |
Terry Jan Reedy | 7cca28f | 2014-07-10 01:16:49 -0400 | [diff] [blame] | 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) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 144 | |
| 145 | def __repr__(self): |
Serhiy Storchaka | 465e60e | 2014-07-25 23:36:00 +0300 | [diff] [blame] | 146 | return "%s(%r, %r)" % (self.__class__.__name__, |
| 147 | self.redir, self.operation) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 148 | |
| 149 | def __call__(self, *args): |
Guido van Rossum | 8ce8a78 | 2007-11-01 19:42:39 +0000 | [diff] [blame] | 150 | return self.tk_call(self.orig_and_operation + args) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 151 | |
| 152 | |
Terry Jan Reedy | 7cca28f | 2014-07-10 01:16:49 -0400 | [diff] [blame] | 153 | def _widget_redirector(parent): # htest # |
Terry Jan Reedy | b60adc5 | 2016-06-21 18:41:38 -0400 | [diff] [blame] | 154 | from tkinter import Toplevel, Text |
Terry Jan Reedy | 7cca28f | 2014-07-10 01:16:49 -0400 | [diff] [blame] | 155 | |
Terry Jan Reedy | b60adc5 | 2016-06-21 18:41:38 -0400 | [diff] [blame] | 156 | top = Toplevel(parent) |
| 157 | top.title("Test WidgetRedirector") |
Terry Jan Reedy | a748032 | 2016-07-10 17:28:10 -0400 | [diff] [blame] | 158 | x, y = map(int, parent.geometry().split('+')[1:]) |
| 159 | top.geometry("+%d+%d" % (x, y + 175)) |
Terry Jan Reedy | b60adc5 | 2016-06-21 18:41:38 -0400 | [diff] [blame] | 160 | text = Text(top) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 161 | text.pack() |
| 162 | text.focus_set() |
| 163 | redir = WidgetRedirector(text) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 164 | def my_insert(*args): |
Guido van Rossum | be19ed7 | 2007-02-09 05:37:30 +0000 | [diff] [blame] | 165 | print("insert", args) |
Terry Jan Reedy | 7cca28f | 2014-07-10 01:16:49 -0400 | [diff] [blame] | 166 | original_insert(*args) |
| 167 | original_insert = redir.register("insert", my_insert) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 168 | |
| 169 | if __name__ == "__main__": |
Terry Jan Reedy | ea3dc80 | 2018-06-18 04:47:59 -0400 | [diff] [blame] | 170 | from unittest import main |
| 171 | main('idlelib.idle_test.test_redirector', verbosity=2, exit=False) |
Terry Jan Reedy | a748032 | 2016-07-10 17:28:10 -0400 | [diff] [blame] | 172 | |
Terry Jan Reedy | 1b392ff | 2014-05-24 18:48:18 -0400 | [diff] [blame] | 173 | from idlelib.idle_test.htest import run |
| 174 | run(_widget_redirector) |