1"""A call-tip window class for Tkinter/IDLE. 2 3After tooltip.py, which uses ideas gleaned from PySol. 4Used by calltip.py. 5""" 6from tkinter import Label, LEFT, SOLID, TclError 7 8from idlelib.tooltip import TooltipBase 9 10HIDE_EVENT = "<<calltipwindow-hide>>" 11HIDE_SEQUENCES = ("<Key-Escape>", "<FocusOut>") 12CHECKHIDE_EVENT = "<<calltipwindow-checkhide>>" 13CHECKHIDE_SEQUENCES = ("<KeyRelease>", "<ButtonRelease>") 14CHECKHIDE_TIME = 100 # milliseconds 15 16MARK_RIGHT = "calltipwindowregion_right" 17 18 19class 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 32 self.hideid = self.checkhideid = None 33 self.checkhide_after_id = None 34 35 def get_position(self): 36 """Choose the position of the call-tip.""" 37 curline = int(self.anchor_widget.index("insert").split('.')[0]) 38 if curline == self.parenline: 39 anchor_index = (self.parenline, self.parencol) 40 else: 41 anchor_index = (curline, 0) 42 box = self.anchor_widget.bbox("%d.%d" % anchor_index) 43 if not box: 44 box = list(self.anchor_widget.bbox("insert")) 45 # align to left of window 46 box[0] = 0 47 box[2] = 0 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() 58 59 def showtip(self, text, parenleft, parenright): 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 66 """ 67 # Only called in calltip.Calltip, where lines are truncated 68 self.text = text 69 if self.tipwindow or not self.text: 70 return 71 72 self.anchor_widget.mark_set(MARK_RIGHT, parenright) 73 self.parenline, self.parencol = map( 74 int, self.anchor_widget.index(parenleft).split(".")) 75 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, 83 background="#ffffd0", foreground="black", 84 relief=SOLID, borderwidth=1, 85 font=self.anchor_widget['font']) 86 self.label.pack() 87 88 def checkhide_event(self, event=None): 89 """Handle CHECK_HIDE_EVENT: call hidetip or reschedule.""" 90 if not self.tipwindow: 91 # If the event was triggered by the same event that unbound 92 # this function, the function will be called nevertheless, 93 # so do nothing in this case. 94 return None 95 96 # Hide the call-tip if the insertion cursor moves outside of the 97 # parenthesis. 98 curline, curcol = map(int, self.anchor_widget.index("insert").split('.')) 99 if curline < self.parenline or \ 100 (curline == self.parenline and curcol <= self.parencol) or \ 101 self.anchor_widget.compare("insert", ">", MARK_RIGHT): 102 self.hidetip() 103 return "break" 104 105 # Not hiding the call-tip. 106 107 self.position_window() 108 # Re-schedule this function to be called again in a short while. 109 if self.checkhide_after_id is not None: 110 self.anchor_widget.after_cancel(self.checkhide_after_id) 111 self.checkhide_after_id = \ 112 self.anchor_widget.after(CHECKHIDE_TIME, self.checkhide_event) 113 return None 114 115 def hide_event(self, event): 116 """Handle HIDE_EVENT by calling hidetip.""" 117 if not self.tipwindow: 118 # See the explanation in checkhide_event. 119 return None 120 self.hidetip() 121 return "break" 122 123 def hidetip(self): 124 """Hide the call-tip.""" 125 if not self.tipwindow: 126 return 127 128 try: 129 self.label.destroy() 130 except TclError: 131 pass 132 self.label = None 133 134 self.parenline = self.parencol = self.lastline = None 135 try: 136 self.anchor_widget.mark_unset(MARK_RIGHT) 137 except TclError: 138 pass 139 140 try: 141 self._unbind_events() 142 except (TclError, ValueError): 143 # ValueError may be raised by MultiCall 144 pass 145 146 super(CalltipWindow, self).hidetip() 147 148 def _bind_events(self): 149 """Bind event handlers.""" 150 self.checkhideid = self.anchor_widget.bind(CHECKHIDE_EVENT, 151 self.checkhide_event) 152 for seq in CHECKHIDE_SEQUENCES: 153 self.anchor_widget.event_add(CHECKHIDE_EVENT, seq) 154 self.anchor_widget.after(CHECKHIDE_TIME, self.checkhide_event) 155 self.hideid = self.anchor_widget.bind(HIDE_EVENT, 156 self.hide_event) 157 for seq in HIDE_SEQUENCES: 158 self.anchor_widget.event_add(HIDE_EVENT, seq) 159 160 def _unbind_events(self): 161 """Unbind event handlers.""" 162 for seq in CHECKHIDE_SEQUENCES: 163 self.anchor_widget.event_delete(CHECKHIDE_EVENT, seq) 164 self.anchor_widget.unbind(CHECKHIDE_EVENT, self.checkhideid) 165 self.checkhideid = None 166 for seq in HIDE_SEQUENCES: 167 self.anchor_widget.event_delete(HIDE_EVENT, seq) 168 self.anchor_widget.unbind(HIDE_EVENT, self.hideid) 169 self.hideid = None 170 171 172def _calltip_window(parent): # htest # 173 from tkinter import Toplevel, Text, LEFT, BOTH 174 175 top = Toplevel(parent) 176 top.title("Test call-tips") 177 x, y = map(int, parent.geometry().split('+')[1:]) 178 top.geometry("250x100+%d+%d" % (x + 175, y + 150)) 179 text = Text(top) 180 text.pack(side=LEFT, fill=BOTH, expand=1) 181 text.insert("insert", "string.split") 182 top.update() 183 184 calltip = CalltipWindow(text) 185 def calltip_show(event): 186 calltip.showtip("(s='Hello world')", "insert", "end") 187 def calltip_hide(event): 188 calltip.hidetip() 189 text.event_add("<<calltip-show>>", "(") 190 text.event_add("<<calltip-hide>>", ")") 191 text.bind("<<calltip-show>>", calltip_show) 192 text.bind("<<calltip-hide>>", calltip_hide) 193 194 text.focus_set() 195 196if __name__ == '__main__': 197 from unittest import main 198 main('idlelib.idle_test.test_calltip_w', verbosity=2, exit=False) 199 200 from idlelib.idle_test.htest import run 201 run(_calltip_window) 202