jpayne@68: """A call-tip window class for Tkinter/IDLE. jpayne@68: jpayne@68: After tooltip.py, which uses ideas gleaned from PySol. jpayne@68: Used by calltip.py. jpayne@68: """ jpayne@68: from tkinter import Label, LEFT, SOLID, TclError jpayne@68: jpayne@68: from idlelib.tooltip import TooltipBase jpayne@68: jpayne@68: HIDE_EVENT = "<>" jpayne@68: HIDE_SEQUENCES = ("", "") jpayne@68: CHECKHIDE_EVENT = "<>" jpayne@68: CHECKHIDE_SEQUENCES = ("", "") jpayne@68: CHECKHIDE_TIME = 100 # milliseconds jpayne@68: jpayne@68: MARK_RIGHT = "calltipwindowregion_right" jpayne@68: jpayne@68: jpayne@68: class CalltipWindow(TooltipBase): jpayne@68: """A call-tip widget for tkinter text widgets.""" jpayne@68: jpayne@68: def __init__(self, text_widget): jpayne@68: """Create a call-tip; shown by showtip(). jpayne@68: jpayne@68: text_widget: a Text widget with code for which call-tips are desired jpayne@68: """ jpayne@68: # Note: The Text widget will be accessible as self.anchor_widget jpayne@68: super(CalltipWindow, self).__init__(text_widget) jpayne@68: jpayne@68: self.label = self.text = None jpayne@68: self.parenline = self.parencol = self.lastline = None jpayne@68: self.hideid = self.checkhideid = None jpayne@68: self.checkhide_after_id = None jpayne@68: jpayne@68: def get_position(self): jpayne@68: """Choose the position of the call-tip.""" jpayne@68: curline = int(self.anchor_widget.index("insert").split('.')[0]) jpayne@68: if curline == self.parenline: jpayne@68: anchor_index = (self.parenline, self.parencol) jpayne@68: else: jpayne@68: anchor_index = (curline, 0) jpayne@68: box = self.anchor_widget.bbox("%d.%d" % anchor_index) jpayne@68: if not box: jpayne@68: box = list(self.anchor_widget.bbox("insert")) jpayne@68: # align to left of window jpayne@68: box[0] = 0 jpayne@68: box[2] = 0 jpayne@68: return box[0] + 2, box[1] + box[3] jpayne@68: jpayne@68: def position_window(self): jpayne@68: "Reposition the window if needed." jpayne@68: curline = int(self.anchor_widget.index("insert").split('.')[0]) jpayne@68: if curline == self.lastline: jpayne@68: return jpayne@68: self.lastline = curline jpayne@68: self.anchor_widget.see("insert") jpayne@68: super(CalltipWindow, self).position_window() jpayne@68: jpayne@68: def showtip(self, text, parenleft, parenright): jpayne@68: """Show the call-tip, bind events which will close it and reposition it. jpayne@68: jpayne@68: text: the text to display in the call-tip jpayne@68: parenleft: index of the opening parenthesis in the text widget jpayne@68: parenright: index of the closing parenthesis in the text widget, jpayne@68: or the end of the line if there is no closing parenthesis jpayne@68: """ jpayne@68: # Only called in calltip.Calltip, where lines are truncated jpayne@68: self.text = text jpayne@68: if self.tipwindow or not self.text: jpayne@68: return jpayne@68: jpayne@68: self.anchor_widget.mark_set(MARK_RIGHT, parenright) jpayne@68: self.parenline, self.parencol = map( jpayne@68: int, self.anchor_widget.index(parenleft).split(".")) jpayne@68: jpayne@68: super(CalltipWindow, self).showtip() jpayne@68: jpayne@68: self._bind_events() jpayne@68: jpayne@68: def showcontents(self): jpayne@68: """Create the call-tip widget.""" jpayne@68: self.label = Label(self.tipwindow, text=self.text, justify=LEFT, jpayne@68: background="#ffffd0", foreground="black", jpayne@68: relief=SOLID, borderwidth=1, jpayne@68: font=self.anchor_widget['font']) jpayne@68: self.label.pack() jpayne@68: jpayne@68: def checkhide_event(self, event=None): jpayne@68: """Handle CHECK_HIDE_EVENT: call hidetip or reschedule.""" jpayne@68: if not self.tipwindow: jpayne@68: # If the event was triggered by the same event that unbound jpayne@68: # this function, the function will be called nevertheless, jpayne@68: # so do nothing in this case. jpayne@68: return None jpayne@68: jpayne@68: # Hide the call-tip if the insertion cursor moves outside of the jpayne@68: # parenthesis. jpayne@68: curline, curcol = map(int, self.anchor_widget.index("insert").split('.')) jpayne@68: if curline < self.parenline or \ jpayne@68: (curline == self.parenline and curcol <= self.parencol) or \ jpayne@68: self.anchor_widget.compare("insert", ">", MARK_RIGHT): jpayne@68: self.hidetip() jpayne@68: return "break" jpayne@68: jpayne@68: # Not hiding the call-tip. jpayne@68: jpayne@68: self.position_window() jpayne@68: # Re-schedule this function to be called again in a short while. jpayne@68: if self.checkhide_after_id is not None: jpayne@68: self.anchor_widget.after_cancel(self.checkhide_after_id) jpayne@68: self.checkhide_after_id = \ jpayne@68: self.anchor_widget.after(CHECKHIDE_TIME, self.checkhide_event) jpayne@68: return None jpayne@68: jpayne@68: def hide_event(self, event): jpayne@68: """Handle HIDE_EVENT by calling hidetip.""" jpayne@68: if not self.tipwindow: jpayne@68: # See the explanation in checkhide_event. jpayne@68: return None jpayne@68: self.hidetip() jpayne@68: return "break" jpayne@68: jpayne@68: def hidetip(self): jpayne@68: """Hide the call-tip.""" jpayne@68: if not self.tipwindow: jpayne@68: return jpayne@68: jpayne@68: try: jpayne@68: self.label.destroy() jpayne@68: except TclError: jpayne@68: pass jpayne@68: self.label = None jpayne@68: jpayne@68: self.parenline = self.parencol = self.lastline = None jpayne@68: try: jpayne@68: self.anchor_widget.mark_unset(MARK_RIGHT) jpayne@68: except TclError: jpayne@68: pass jpayne@68: jpayne@68: try: jpayne@68: self._unbind_events() jpayne@68: except (TclError, ValueError): jpayne@68: # ValueError may be raised by MultiCall jpayne@68: pass jpayne@68: jpayne@68: super(CalltipWindow, self).hidetip() jpayne@68: jpayne@68: def _bind_events(self): jpayne@68: """Bind event handlers.""" jpayne@68: self.checkhideid = self.anchor_widget.bind(CHECKHIDE_EVENT, jpayne@68: self.checkhide_event) jpayne@68: for seq in CHECKHIDE_SEQUENCES: jpayne@68: self.anchor_widget.event_add(CHECKHIDE_EVENT, seq) jpayne@68: self.anchor_widget.after(CHECKHIDE_TIME, self.checkhide_event) jpayne@68: self.hideid = self.anchor_widget.bind(HIDE_EVENT, jpayne@68: self.hide_event) jpayne@68: for seq in HIDE_SEQUENCES: jpayne@68: self.anchor_widget.event_add(HIDE_EVENT, seq) jpayne@68: jpayne@68: def _unbind_events(self): jpayne@68: """Unbind event handlers.""" jpayne@68: for seq in CHECKHIDE_SEQUENCES: jpayne@68: self.anchor_widget.event_delete(CHECKHIDE_EVENT, seq) jpayne@68: self.anchor_widget.unbind(CHECKHIDE_EVENT, self.checkhideid) jpayne@68: self.checkhideid = None jpayne@68: for seq in HIDE_SEQUENCES: jpayne@68: self.anchor_widget.event_delete(HIDE_EVENT, seq) jpayne@68: self.anchor_widget.unbind(HIDE_EVENT, self.hideid) jpayne@68: self.hideid = None jpayne@68: jpayne@68: jpayne@68: def _calltip_window(parent): # htest # jpayne@68: from tkinter import Toplevel, Text, LEFT, BOTH jpayne@68: jpayne@68: top = Toplevel(parent) jpayne@68: top.title("Test call-tips") jpayne@68: x, y = map(int, parent.geometry().split('+')[1:]) jpayne@68: top.geometry("250x100+%d+%d" % (x + 175, y + 150)) jpayne@68: text = Text(top) jpayne@68: text.pack(side=LEFT, fill=BOTH, expand=1) jpayne@68: text.insert("insert", "string.split") jpayne@68: top.update() jpayne@68: jpayne@68: calltip = CalltipWindow(text) jpayne@68: def calltip_show(event): jpayne@68: calltip.showtip("(s='Hello world')", "insert", "end") jpayne@68: def calltip_hide(event): jpayne@68: calltip.hidetip() jpayne@68: text.event_add("<>", "(") jpayne@68: text.event_add("<>", ")") jpayne@68: text.bind("<>", calltip_show) jpayne@68: text.bind("<>", calltip_hide) jpayne@68: jpayne@68: text.focus_set() jpayne@68: jpayne@68: if __name__ == '__main__': jpayne@68: from unittest import main jpayne@68: main('idlelib.idle_test.test_calltip_w', verbosity=2, exit=False) jpayne@68: jpayne@68: from idlelib.idle_test.htest import run jpayne@68: run(_calltip_window)