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