1"""A CallTip window class for Tkinter/IDLE. 2 3After ToolTip.py, which uses ideas gleaned from PySol 4Used by the CallTips IDLE extension. 5""" 6from Tkinter import Toplevel, Label, LEFT, SOLID, TclError 7 8HIDE_VIRTUAL_EVENT_NAME = "<<calltipwindow-hide>>" 9HIDE_SEQUENCES = ("<Key-Escape>", "<FocusOut>") 10CHECKHIDE_VIRTUAL_EVENT_NAME = "<<calltipwindow-checkhide>>" 11CHECKHIDE_SEQUENCES = ("<KeyRelease>", "<ButtonRelease>") 12CHECKHIDE_TIME = 100 # milliseconds 13 14MARK_RIGHT = "calltipwindowregion_right" 15 16class CallTip: 17 18 def __init__(self, widget): 19 self.widget = widget 20 self.tipwindow = self.label = None 21 self.parenline = self.parencol = None 22 self.lastline = None 23 self.hideid = self.checkhideid = None 24 self.checkhide_after_id = None 25 26 def position_window(self): 27 """Check if needs to reposition the window, and if so - do it.""" 28 curline = int(self.widget.index("insert").split('.')[0]) 29 if curline == self.lastline: 30 return 31 self.lastline = curline 32 self.widget.see("insert") 33 if curline == self.parenline: 34 box = self.widget.bbox("%d.%d" % (self.parenline, 35 self.parencol)) 36 else: 37 box = self.widget.bbox("%d.0" % curline) 38 if not box: 39 box = list(self.widget.bbox("insert")) 40 # align to left of window 41 box[0] = 0 42 box[2] = 0 43 x = box[0] + self.widget.winfo_rootx() + 2 44 y = box[1] + box[3] + self.widget.winfo_rooty() 45 self.tipwindow.wm_geometry("+%d+%d" % (x, y)) 46 47 def showtip(self, text, parenleft, parenright): 48 """Show the calltip, bind events which will close it and reposition it. 49 """ 50 # Only called in CallTips, where lines are truncated 51 self.text = text 52 if self.tipwindow or not self.text: 53 return 54 55 self.widget.mark_set(MARK_RIGHT, parenright) 56 self.parenline, self.parencol = map( 57 int, self.widget.index(parenleft).split(".")) 58 59 self.tipwindow = tw = Toplevel(self.widget) 60 self.position_window() 61 # remove border on calltip window 62 tw.wm_overrideredirect(1) 63 try: 64 # This command is only needed and available on Tk >= 8.4.0 for OSX 65 # Without it, call tips intrude on the typing process by grabbing 66 # the focus. 67 tw.tk.call("::tk::unsupported::MacWindowStyle", "style", tw._w, 68 "help", "noActivates") 69 except TclError: 70 pass 71 self.label = Label(tw, text=self.text, justify=LEFT, 72 background="#ffffe0", relief=SOLID, borderwidth=1, 73 font = self.widget['font']) 74 self.label.pack() 75 tw.update_idletasks() 76 tw.lift() # work around bug in Tk 8.5.18+ (issue #24570) 77 78 self.checkhideid = self.widget.bind(CHECKHIDE_VIRTUAL_EVENT_NAME, 79 self.checkhide_event) 80 for seq in CHECKHIDE_SEQUENCES: 81 self.widget.event_add(CHECKHIDE_VIRTUAL_EVENT_NAME, seq) 82 self.widget.after(CHECKHIDE_TIME, self.checkhide_event) 83 self.hideid = self.widget.bind(HIDE_VIRTUAL_EVENT_NAME, 84 self.hide_event) 85 for seq in HIDE_SEQUENCES: 86 self.widget.event_add(HIDE_VIRTUAL_EVENT_NAME, seq) 87 88 def checkhide_event(self, event=None): 89 if not self.tipwindow: 90 # If the event was triggered by the same event that unbinded 91 # this function, the function will be called nevertheless, 92 # so do nothing in this case. 93 return 94 curline, curcol = map(int, self.widget.index("insert").split('.')) 95 if curline < self.parenline or \ 96 (curline == self.parenline and curcol <= self.parencol) or \ 97 self.widget.compare("insert", ">", MARK_RIGHT): 98 self.hidetip() 99 else: 100 self.position_window() 101 if self.checkhide_after_id is not None: 102 self.widget.after_cancel(self.checkhide_after_id) 103 self.checkhide_after_id = \ 104 self.widget.after(CHECKHIDE_TIME, self.checkhide_event) 105 106 def hide_event(self, event): 107 if not self.tipwindow: 108 # See the explanation in checkhide_event. 109 return 110 self.hidetip() 111 112 def hidetip(self): 113 if not self.tipwindow: 114 return 115 116 for seq in CHECKHIDE_SEQUENCES: 117 self.widget.event_delete(CHECKHIDE_VIRTUAL_EVENT_NAME, seq) 118 self.widget.unbind(CHECKHIDE_VIRTUAL_EVENT_NAME, self.checkhideid) 119 self.checkhideid = None 120 for seq in HIDE_SEQUENCES: 121 self.widget.event_delete(HIDE_VIRTUAL_EVENT_NAME, seq) 122 self.widget.unbind(HIDE_VIRTUAL_EVENT_NAME, self.hideid) 123 self.hideid = None 124 125 self.label.destroy() 126 self.label = None 127 self.tipwindow.destroy() 128 self.tipwindow = None 129 130 self.widget.mark_unset(MARK_RIGHT) 131 self.parenline = self.parencol = self.lastline = None 132 133 def is_active(self): 134 return bool(self.tipwindow) 135 136 137def _calltip_window(parent): # htest # 138 from Tkinter import Toplevel, Text, LEFT, BOTH 139 140 top = Toplevel(parent) 141 top.title("Test calltips") 142 top.geometry("200x100+%d+%d" % (parent.winfo_rootx() + 200, 143 parent.winfo_rooty() + 150)) 144 text = Text(top) 145 text.pack(side=LEFT, fill=BOTH, expand=1) 146 text.insert("insert", "string.split") 147 top.update() 148 calltip = CallTip(text) 149 150 def calltip_show(event): 151 calltip.showtip("(s=Hello world)", "insert", "end") 152 def calltip_hide(event): 153 calltip.hidetip() 154 text.event_add("<<calltip-show>>", "(") 155 text.event_add("<<calltip-hide>>", ")") 156 text.bind("<<calltip-show>>", calltip_show) 157 text.bind("<<calltip-hide>>", calltip_hide) 158 text.focus_set() 159 160if __name__=='__main__': 161 from idlelib.idle_test.htest import run 162 run(_calltip_window) 163