Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 1 | """A call-tip window class for Tkinter/IDLE. |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 2 | |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 3 | After tooltip.py, which uses ideas gleaned from PySol. |
| 4 | Used by calltip.py. |
Kurt B. Kaiser | e54710b | 2002-12-12 19:15:39 +0000 | [diff] [blame] | 5 | """ |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 6 | from tkinter import Label, LEFT, SOLID, TclError |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 7 | |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 8 | from idlelib.tooltip import TooltipBase |
| 9 | |
| 10 | HIDE_EVENT = "<<calltipwindow-hide>>" |
Kurt B. Kaiser | b175445 | 2005-11-18 22:05:48 +0000 | [diff] [blame] | 11 | HIDE_SEQUENCES = ("<Key-Escape>", "<FocusOut>") |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 12 | CHECKHIDE_EVENT = "<<calltipwindow-checkhide>>" |
Kurt B. Kaiser | b175445 | 2005-11-18 22:05:48 +0000 | [diff] [blame] | 13 | CHECKHIDE_SEQUENCES = ("<KeyRelease>", "<ButtonRelease>") |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 14 | CHECKHIDE_TIME = 100 # milliseconds |
Kurt B. Kaiser | b175445 | 2005-11-18 22:05:48 +0000 | [diff] [blame] | 15 | |
| 16 | MARK_RIGHT = "calltipwindowregion_right" |
| 17 | |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 18 | |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 19 | class CalltipWindow(TooltipBase): |
| 20 | """A call-tip widget for tkinter text widgets.""" |
| 21 | |
| 22 | def __init__(self, text_widget): |
| 23 | """Create a call-tip; shown by showtip(). |
| 24 | |
| 25 | text_widget: a Text widget with code for which call-tips are desired |
| 26 | """ |
| 27 | # Note: The Text widget will be accessible as self.anchor_widget |
| 28 | super(CalltipWindow, self).__init__(text_widget) |
| 29 | |
| 30 | self.label = self.text = None |
| 31 | self.parenline = self.parencol = self.lastline = None |
Kurt B. Kaiser | b175445 | 2005-11-18 22:05:48 +0000 | [diff] [blame] | 32 | self.hideid = self.checkhideid = None |
Martin v. Löwis | ee381a0 | 2012-03-13 14:18:36 -0700 | [diff] [blame] | 33 | self.checkhide_after_id = None |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 34 | |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 35 | def get_position(self): |
| 36 | """Choose the position of the call-tip.""" |
| 37 | curline = int(self.anchor_widget.index("insert").split('.')[0]) |
Kurt B. Kaiser | b175445 | 2005-11-18 22:05:48 +0000 | [diff] [blame] | 38 | if curline == self.parenline: |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 39 | anchor_index = (self.parenline, self.parencol) |
Kurt B. Kaiser | b175445 | 2005-11-18 22:05:48 +0000 | [diff] [blame] | 40 | else: |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 41 | anchor_index = (curline, 0) |
| 42 | box = self.anchor_widget.bbox("%d.%d" % anchor_index) |
Kurt B. Kaiser | b175445 | 2005-11-18 22:05:48 +0000 | [diff] [blame] | 43 | if not box: |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 44 | box = list(self.anchor_widget.bbox("insert")) |
Kurt B. Kaiser | b175445 | 2005-11-18 22:05:48 +0000 | [diff] [blame] | 45 | # align to left of window |
| 46 | box[0] = 0 |
| 47 | box[2] = 0 |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 48 | return box[0] + 2, box[1] + box[3] |
| 49 | |
| 50 | def position_window(self): |
| 51 | "Reposition the window if needed." |
| 52 | curline = int(self.anchor_widget.index("insert").split('.')[0]) |
| 53 | if curline == self.lastline: |
| 54 | return |
| 55 | self.lastline = curline |
| 56 | self.anchor_widget.see("insert") |
| 57 | super(CalltipWindow, self).position_window() |
Kurt B. Kaiser | b175445 | 2005-11-18 22:05:48 +0000 | [diff] [blame] | 58 | |
| 59 | def showtip(self, text, parenleft, parenright): |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 60 | """Show the call-tip, bind events which will close it and reposition it. |
| 61 | |
| 62 | text: the text to display in the call-tip |
| 63 | parenleft: index of the opening parenthesis in the text widget |
| 64 | parenright: index of the closing parenthesis in the text widget, |
| 65 | or the end of the line if there is no closing parenthesis |
Kurt B. Kaiser | b175445 | 2005-11-18 22:05:48 +0000 | [diff] [blame] | 66 | """ |
Terry Jan Reedy | 9af1836 | 2018-06-20 02:18:49 -0400 | [diff] [blame] | 67 | # Only called in calltip.Calltip, where lines are truncated |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 68 | self.text = text |
| 69 | if self.tipwindow or not self.text: |
| 70 | return |
Kurt B. Kaiser | b175445 | 2005-11-18 22:05:48 +0000 | [diff] [blame] | 71 | |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 72 | self.anchor_widget.mark_set(MARK_RIGHT, parenright) |
Kurt B. Kaiser | b175445 | 2005-11-18 22:05:48 +0000 | [diff] [blame] | 73 | self.parenline, self.parencol = map( |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 74 | int, self.anchor_widget.index(parenleft).split(".")) |
Kurt B. Kaiser | b175445 | 2005-11-18 22:05:48 +0000 | [diff] [blame] | 75 | |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 76 | super(CalltipWindow, self).showtip() |
| 77 | |
| 78 | self._bind_events() |
| 79 | |
| 80 | def showcontents(self): |
| 81 | """Create the call-tip widget.""" |
| 82 | self.label = Label(self.tipwindow, text=self.text, justify=LEFT, |
Kurt B. Kaiser | b175445 | 2005-11-18 22:05:48 +0000 | [diff] [blame] | 83 | background="#ffffe0", relief=SOLID, borderwidth=1, |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 84 | font=self.anchor_widget['font']) |
Kurt B. Kaiser | b175445 | 2005-11-18 22:05:48 +0000 | [diff] [blame] | 85 | self.label.pack() |
Kurt B. Kaiser | b175445 | 2005-11-18 22:05:48 +0000 | [diff] [blame] | 86 | |
| 87 | def checkhide_event(self, event=None): |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 88 | """Handle CHECK_HIDE_EVENT: call hidetip or reschedule.""" |
Kurt B. Kaiser | b175445 | 2005-11-18 22:05:48 +0000 | [diff] [blame] | 89 | if not self.tipwindow: |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 90 | # If the event was triggered by the same event that unbound |
Kurt B. Kaiser | b175445 | 2005-11-18 22:05:48 +0000 | [diff] [blame] | 91 | # this function, the function will be called nevertheless, |
| 92 | # so do nothing in this case. |
Serhiy Storchaka | 213ce12 | 2017-06-27 07:02:32 +0300 | [diff] [blame] | 93 | return None |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 94 | |
| 95 | # Hide the call-tip if the insertion cursor moves outside of the |
| 96 | # parenthesis. |
| 97 | curline, curcol = map(int, self.anchor_widget.index("insert").split('.')) |
Kurt B. Kaiser | b175445 | 2005-11-18 22:05:48 +0000 | [diff] [blame] | 98 | if curline < self.parenline or \ |
| 99 | (curline == self.parenline and curcol <= self.parencol) or \ |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 100 | self.anchor_widget.compare("insert", ">", MARK_RIGHT): |
Kurt B. Kaiser | b175445 | 2005-11-18 22:05:48 +0000 | [diff] [blame] | 101 | self.hidetip() |
Serhiy Storchaka | 213ce12 | 2017-06-27 07:02:32 +0300 | [diff] [blame] | 102 | return "break" |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 103 | |
| 104 | # Not hiding the call-tip. |
| 105 | |
| 106 | self.position_window() |
| 107 | # Re-schedule this function to be called again in a short while. |
| 108 | if self.checkhide_after_id is not None: |
| 109 | self.anchor_widget.after_cancel(self.checkhide_after_id) |
| 110 | self.checkhide_after_id = \ |
| 111 | self.anchor_widget.after(CHECKHIDE_TIME, self.checkhide_event) |
| 112 | return None |
Kurt B. Kaiser | b175445 | 2005-11-18 22:05:48 +0000 | [diff] [blame] | 113 | |
| 114 | def hide_event(self, event): |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 115 | """Handle HIDE_EVENT by calling hidetip.""" |
Kurt B. Kaiser | b175445 | 2005-11-18 22:05:48 +0000 | [diff] [blame] | 116 | if not self.tipwindow: |
| 117 | # See the explanation in checkhide_event. |
Serhiy Storchaka | 213ce12 | 2017-06-27 07:02:32 +0300 | [diff] [blame] | 118 | return None |
Kurt B. Kaiser | b175445 | 2005-11-18 22:05:48 +0000 | [diff] [blame] | 119 | self.hidetip() |
Serhiy Storchaka | 213ce12 | 2017-06-27 07:02:32 +0300 | [diff] [blame] | 120 | return "break" |
Kurt B. Kaiser | 9a1ae1a | 2001-07-12 22:26:44 +0000 | [diff] [blame] | 121 | |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 122 | def hidetip(self): |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 123 | """Hide the call-tip.""" |
Kurt B. Kaiser | b175445 | 2005-11-18 22:05:48 +0000 | [diff] [blame] | 124 | if not self.tipwindow: |
| 125 | return |
| 126 | |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 127 | try: |
| 128 | self.label.destroy() |
| 129 | except TclError: |
| 130 | pass |
| 131 | self.label = None |
| 132 | |
| 133 | self.parenline = self.parencol = self.lastline = None |
| 134 | try: |
| 135 | self.anchor_widget.mark_unset(MARK_RIGHT) |
| 136 | except TclError: |
| 137 | pass |
| 138 | |
| 139 | try: |
| 140 | self._unbind_events() |
| 141 | except (TclError, ValueError): |
| 142 | # ValueError may be raised by MultiCall |
| 143 | pass |
| 144 | |
| 145 | super(CalltipWindow, self).hidetip() |
| 146 | |
| 147 | def _bind_events(self): |
| 148 | """Bind event handlers.""" |
| 149 | self.checkhideid = self.anchor_widget.bind(CHECKHIDE_EVENT, |
| 150 | self.checkhide_event) |
Kurt B. Kaiser | b175445 | 2005-11-18 22:05:48 +0000 | [diff] [blame] | 151 | for seq in CHECKHIDE_SEQUENCES: |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 152 | self.anchor_widget.event_add(CHECKHIDE_EVENT, seq) |
| 153 | self.anchor_widget.after(CHECKHIDE_TIME, self.checkhide_event) |
| 154 | self.hideid = self.anchor_widget.bind(HIDE_EVENT, |
| 155 | self.hide_event) |
| 156 | for seq in HIDE_SEQUENCES: |
| 157 | self.anchor_widget.event_add(HIDE_EVENT, seq) |
| 158 | |
| 159 | def _unbind_events(self): |
| 160 | """Unbind event handlers.""" |
| 161 | for seq in CHECKHIDE_SEQUENCES: |
| 162 | self.anchor_widget.event_delete(CHECKHIDE_EVENT, seq) |
| 163 | self.anchor_widget.unbind(CHECKHIDE_EVENT, self.checkhideid) |
Kurt B. Kaiser | b175445 | 2005-11-18 22:05:48 +0000 | [diff] [blame] | 164 | self.checkhideid = None |
| 165 | for seq in HIDE_SEQUENCES: |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 166 | self.anchor_widget.event_delete(HIDE_EVENT, seq) |
| 167 | self.anchor_widget.unbind(HIDE_EVENT, self.hideid) |
Kurt B. Kaiser | b175445 | 2005-11-18 22:05:48 +0000 | [diff] [blame] | 168 | self.hideid = None |
| 169 | |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 170 | |
Terry Jan Reedy | 44f09eb | 2014-07-01 18:52:37 -0400 | [diff] [blame] | 171 | def _calltip_window(parent): # htest # |
Terry Jan Reedy | cd56736 | 2014-10-17 01:31:35 -0400 | [diff] [blame] | 172 | from tkinter import Toplevel, Text, LEFT, BOTH |
Terry Jan Reedy | 44f09eb | 2014-07-01 18:52:37 -0400 | [diff] [blame] | 173 | |
Terry Jan Reedy | cd56736 | 2014-10-17 01:31:35 -0400 | [diff] [blame] | 174 | top = Toplevel(parent) |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 175 | top.title("Test call-tips") |
Terry Jan Reedy | a748032 | 2016-07-10 17:28:10 -0400 | [diff] [blame] | 176 | x, y = map(int, parent.geometry().split('+')[1:]) |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 177 | top.geometry("250x100+%d+%d" % (x + 175, y + 150)) |
Terry Jan Reedy | cd56736 | 2014-10-17 01:31:35 -0400 | [diff] [blame] | 178 | text = Text(top) |
| 179 | text.pack(side=LEFT, fill=BOTH, expand=1) |
| 180 | text.insert("insert", "string.split") |
| 181 | top.update() |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 182 | |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 183 | calltip = CalltipWindow(text) |
Terry Jan Reedy | cd56736 | 2014-10-17 01:31:35 -0400 | [diff] [blame] | 184 | def calltip_show(event): |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 185 | calltip.showtip("(s='Hello world')", "insert", "end") |
Terry Jan Reedy | cd56736 | 2014-10-17 01:31:35 -0400 | [diff] [blame] | 186 | def calltip_hide(event): |
| 187 | calltip.hidetip() |
| 188 | text.event_add("<<calltip-show>>", "(") |
| 189 | text.event_add("<<calltip-hide>>", ")") |
| 190 | text.bind("<<calltip-show>>", calltip_show) |
| 191 | text.bind("<<calltip-hide>>", calltip_hide) |
Tal Einat | 87e59ac | 2018-08-05 09:21:08 +0300 | [diff] [blame] | 192 | |
Terry Jan Reedy | cd56736 | 2014-10-17 01:31:35 -0400 | [diff] [blame] | 193 | text.focus_set() |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 194 | |
Terry Jan Reedy | ee5ef30 | 2018-06-15 18:20:55 -0400 | [diff] [blame] | 195 | if __name__ == '__main__': |
| 196 | from unittest import main |
Terry Jan Reedy | 06e2029 | 2018-06-19 23:00:35 -0400 | [diff] [blame] | 197 | main('idlelib.idle_test.test_calltip_w', verbosity=2, exit=False) |
Terry Jan Reedy | ee5ef30 | 2018-06-15 18:20:55 -0400 | [diff] [blame] | 198 | |
Terry Jan Reedy | 1b392ff | 2014-05-24 18:48:18 -0400 | [diff] [blame] | 199 | from idlelib.idle_test.htest import run |
| 200 | run(_calltip_window) |