17db96d56Sopenharmony_ci"""A call-tip window class for Tkinter/IDLE.
27db96d56Sopenharmony_ci
37db96d56Sopenharmony_ciAfter tooltip.py, which uses ideas gleaned from PySol.
47db96d56Sopenharmony_ciUsed by calltip.py.
57db96d56Sopenharmony_ci"""
67db96d56Sopenharmony_cifrom tkinter import Label, LEFT, SOLID, TclError
77db96d56Sopenharmony_ci
87db96d56Sopenharmony_cifrom idlelib.tooltip import TooltipBase
97db96d56Sopenharmony_ci
107db96d56Sopenharmony_ciHIDE_EVENT = "<<calltipwindow-hide>>"
117db96d56Sopenharmony_ciHIDE_SEQUENCES = ("<Key-Escape>", "<FocusOut>")
127db96d56Sopenharmony_ciCHECKHIDE_EVENT = "<<calltipwindow-checkhide>>"
137db96d56Sopenharmony_ciCHECKHIDE_SEQUENCES = ("<KeyRelease>", "<ButtonRelease>")
147db96d56Sopenharmony_ciCHECKHIDE_TIME = 100  # milliseconds
157db96d56Sopenharmony_ci
167db96d56Sopenharmony_ciMARK_RIGHT = "calltipwindowregion_right"
177db96d56Sopenharmony_ci
187db96d56Sopenharmony_ci
197db96d56Sopenharmony_ciclass CalltipWindow(TooltipBase):
207db96d56Sopenharmony_ci    """A call-tip widget for tkinter text widgets."""
217db96d56Sopenharmony_ci
227db96d56Sopenharmony_ci    def __init__(self, text_widget):
237db96d56Sopenharmony_ci        """Create a call-tip; shown by showtip().
247db96d56Sopenharmony_ci
257db96d56Sopenharmony_ci        text_widget: a Text widget with code for which call-tips are desired
267db96d56Sopenharmony_ci        """
277db96d56Sopenharmony_ci        # Note: The Text widget will be accessible as self.anchor_widget
287db96d56Sopenharmony_ci        super().__init__(text_widget)
297db96d56Sopenharmony_ci
307db96d56Sopenharmony_ci        self.label = self.text = None
317db96d56Sopenharmony_ci        self.parenline = self.parencol = self.lastline = None
327db96d56Sopenharmony_ci        self.hideid = self.checkhideid = None
337db96d56Sopenharmony_ci        self.checkhide_after_id = None
347db96d56Sopenharmony_ci
357db96d56Sopenharmony_ci    def get_position(self):
367db96d56Sopenharmony_ci        """Choose the position of the call-tip."""
377db96d56Sopenharmony_ci        curline = int(self.anchor_widget.index("insert").split('.')[0])
387db96d56Sopenharmony_ci        if curline == self.parenline:
397db96d56Sopenharmony_ci            anchor_index = (self.parenline, self.parencol)
407db96d56Sopenharmony_ci        else:
417db96d56Sopenharmony_ci            anchor_index = (curline, 0)
427db96d56Sopenharmony_ci        box = self.anchor_widget.bbox("%d.%d" % anchor_index)
437db96d56Sopenharmony_ci        if not box:
447db96d56Sopenharmony_ci            box = list(self.anchor_widget.bbox("insert"))
457db96d56Sopenharmony_ci            # align to left of window
467db96d56Sopenharmony_ci            box[0] = 0
477db96d56Sopenharmony_ci            box[2] = 0
487db96d56Sopenharmony_ci        return box[0] + 2, box[1] + box[3]
497db96d56Sopenharmony_ci
507db96d56Sopenharmony_ci    def position_window(self):
517db96d56Sopenharmony_ci        "Reposition the window if needed."
527db96d56Sopenharmony_ci        curline = int(self.anchor_widget.index("insert").split('.')[0])
537db96d56Sopenharmony_ci        if curline == self.lastline:
547db96d56Sopenharmony_ci            return
557db96d56Sopenharmony_ci        self.lastline = curline
567db96d56Sopenharmony_ci        self.anchor_widget.see("insert")
577db96d56Sopenharmony_ci        super().position_window()
587db96d56Sopenharmony_ci
597db96d56Sopenharmony_ci    def showtip(self, text, parenleft, parenright):
607db96d56Sopenharmony_ci        """Show the call-tip, bind events which will close it and reposition it.
617db96d56Sopenharmony_ci
627db96d56Sopenharmony_ci        text: the text to display in the call-tip
637db96d56Sopenharmony_ci        parenleft: index of the opening parenthesis in the text widget
647db96d56Sopenharmony_ci        parenright: index of the closing parenthesis in the text widget,
657db96d56Sopenharmony_ci                    or the end of the line if there is no closing parenthesis
667db96d56Sopenharmony_ci        """
677db96d56Sopenharmony_ci        # Only called in calltip.Calltip, where lines are truncated
687db96d56Sopenharmony_ci        self.text = text
697db96d56Sopenharmony_ci        if self.tipwindow or not self.text:
707db96d56Sopenharmony_ci            return
717db96d56Sopenharmony_ci
727db96d56Sopenharmony_ci        self.anchor_widget.mark_set(MARK_RIGHT, parenright)
737db96d56Sopenharmony_ci        self.parenline, self.parencol = map(
747db96d56Sopenharmony_ci            int, self.anchor_widget.index(parenleft).split("."))
757db96d56Sopenharmony_ci
767db96d56Sopenharmony_ci        super().showtip()
777db96d56Sopenharmony_ci
787db96d56Sopenharmony_ci        self._bind_events()
797db96d56Sopenharmony_ci
807db96d56Sopenharmony_ci    def showcontents(self):
817db96d56Sopenharmony_ci        """Create the call-tip widget."""
827db96d56Sopenharmony_ci        self.label = Label(self.tipwindow, text=self.text, justify=LEFT,
837db96d56Sopenharmony_ci                           background="#ffffd0", foreground="black",
847db96d56Sopenharmony_ci                           relief=SOLID, borderwidth=1,
857db96d56Sopenharmony_ci                           font=self.anchor_widget['font'])
867db96d56Sopenharmony_ci        self.label.pack()
877db96d56Sopenharmony_ci
887db96d56Sopenharmony_ci    def checkhide_event(self, event=None):
897db96d56Sopenharmony_ci        """Handle CHECK_HIDE_EVENT: call hidetip or reschedule."""
907db96d56Sopenharmony_ci        if not self.tipwindow:
917db96d56Sopenharmony_ci            # If the event was triggered by the same event that unbound
927db96d56Sopenharmony_ci            # this function, the function will be called nevertheless,
937db96d56Sopenharmony_ci            # so do nothing in this case.
947db96d56Sopenharmony_ci            return None
957db96d56Sopenharmony_ci
967db96d56Sopenharmony_ci        # Hide the call-tip if the insertion cursor moves outside of the
977db96d56Sopenharmony_ci        # parenthesis.
987db96d56Sopenharmony_ci        curline, curcol = map(int, self.anchor_widget.index("insert").split('.'))
997db96d56Sopenharmony_ci        if curline < self.parenline or \
1007db96d56Sopenharmony_ci           (curline == self.parenline and curcol <= self.parencol) or \
1017db96d56Sopenharmony_ci           self.anchor_widget.compare("insert", ">", MARK_RIGHT):
1027db96d56Sopenharmony_ci            self.hidetip()
1037db96d56Sopenharmony_ci            return "break"
1047db96d56Sopenharmony_ci
1057db96d56Sopenharmony_ci        # Not hiding the call-tip.
1067db96d56Sopenharmony_ci
1077db96d56Sopenharmony_ci        self.position_window()
1087db96d56Sopenharmony_ci        # Re-schedule this function to be called again in a short while.
1097db96d56Sopenharmony_ci        if self.checkhide_after_id is not None:
1107db96d56Sopenharmony_ci            self.anchor_widget.after_cancel(self.checkhide_after_id)
1117db96d56Sopenharmony_ci        self.checkhide_after_id = \
1127db96d56Sopenharmony_ci            self.anchor_widget.after(CHECKHIDE_TIME, self.checkhide_event)
1137db96d56Sopenharmony_ci        return None
1147db96d56Sopenharmony_ci
1157db96d56Sopenharmony_ci    def hide_event(self, event):
1167db96d56Sopenharmony_ci        """Handle HIDE_EVENT by calling hidetip."""
1177db96d56Sopenharmony_ci        if not self.tipwindow:
1187db96d56Sopenharmony_ci            # See the explanation in checkhide_event.
1197db96d56Sopenharmony_ci            return None
1207db96d56Sopenharmony_ci        self.hidetip()
1217db96d56Sopenharmony_ci        return "break"
1227db96d56Sopenharmony_ci
1237db96d56Sopenharmony_ci    def hidetip(self):
1247db96d56Sopenharmony_ci        """Hide the call-tip."""
1257db96d56Sopenharmony_ci        if not self.tipwindow:
1267db96d56Sopenharmony_ci            return
1277db96d56Sopenharmony_ci
1287db96d56Sopenharmony_ci        try:
1297db96d56Sopenharmony_ci            self.label.destroy()
1307db96d56Sopenharmony_ci        except TclError:
1317db96d56Sopenharmony_ci            pass
1327db96d56Sopenharmony_ci        self.label = None
1337db96d56Sopenharmony_ci
1347db96d56Sopenharmony_ci        self.parenline = self.parencol = self.lastline = None
1357db96d56Sopenharmony_ci        try:
1367db96d56Sopenharmony_ci            self.anchor_widget.mark_unset(MARK_RIGHT)
1377db96d56Sopenharmony_ci        except TclError:
1387db96d56Sopenharmony_ci            pass
1397db96d56Sopenharmony_ci
1407db96d56Sopenharmony_ci        try:
1417db96d56Sopenharmony_ci            self._unbind_events()
1427db96d56Sopenharmony_ci        except (TclError, ValueError):
1437db96d56Sopenharmony_ci            # ValueError may be raised by MultiCall
1447db96d56Sopenharmony_ci            pass
1457db96d56Sopenharmony_ci
1467db96d56Sopenharmony_ci        super().hidetip()
1477db96d56Sopenharmony_ci
1487db96d56Sopenharmony_ci    def _bind_events(self):
1497db96d56Sopenharmony_ci        """Bind event handlers."""
1507db96d56Sopenharmony_ci        self.checkhideid = self.anchor_widget.bind(CHECKHIDE_EVENT,
1517db96d56Sopenharmony_ci                                                   self.checkhide_event)
1527db96d56Sopenharmony_ci        for seq in CHECKHIDE_SEQUENCES:
1537db96d56Sopenharmony_ci            self.anchor_widget.event_add(CHECKHIDE_EVENT, seq)
1547db96d56Sopenharmony_ci        self.anchor_widget.after(CHECKHIDE_TIME, self.checkhide_event)
1557db96d56Sopenharmony_ci        self.hideid = self.anchor_widget.bind(HIDE_EVENT,
1567db96d56Sopenharmony_ci                                              self.hide_event)
1577db96d56Sopenharmony_ci        for seq in HIDE_SEQUENCES:
1587db96d56Sopenharmony_ci            self.anchor_widget.event_add(HIDE_EVENT, seq)
1597db96d56Sopenharmony_ci
1607db96d56Sopenharmony_ci    def _unbind_events(self):
1617db96d56Sopenharmony_ci        """Unbind event handlers."""
1627db96d56Sopenharmony_ci        for seq in CHECKHIDE_SEQUENCES:
1637db96d56Sopenharmony_ci            self.anchor_widget.event_delete(CHECKHIDE_EVENT, seq)
1647db96d56Sopenharmony_ci        self.anchor_widget.unbind(CHECKHIDE_EVENT, self.checkhideid)
1657db96d56Sopenharmony_ci        self.checkhideid = None
1667db96d56Sopenharmony_ci        for seq in HIDE_SEQUENCES:
1677db96d56Sopenharmony_ci            self.anchor_widget.event_delete(HIDE_EVENT, seq)
1687db96d56Sopenharmony_ci        self.anchor_widget.unbind(HIDE_EVENT, self.hideid)
1697db96d56Sopenharmony_ci        self.hideid = None
1707db96d56Sopenharmony_ci
1717db96d56Sopenharmony_ci
1727db96d56Sopenharmony_cidef _calltip_window(parent):  # htest #
1737db96d56Sopenharmony_ci    from tkinter import Toplevel, Text, LEFT, BOTH
1747db96d56Sopenharmony_ci
1757db96d56Sopenharmony_ci    top = Toplevel(parent)
1767db96d56Sopenharmony_ci    top.title("Test call-tips")
1777db96d56Sopenharmony_ci    x, y = map(int, parent.geometry().split('+')[1:])
1787db96d56Sopenharmony_ci    top.geometry("250x100+%d+%d" % (x + 175, y + 150))
1797db96d56Sopenharmony_ci    text = Text(top)
1807db96d56Sopenharmony_ci    text.pack(side=LEFT, fill=BOTH, expand=1)
1817db96d56Sopenharmony_ci    text.insert("insert", "string.split")
1827db96d56Sopenharmony_ci    top.update()
1837db96d56Sopenharmony_ci
1847db96d56Sopenharmony_ci    calltip = CalltipWindow(text)
1857db96d56Sopenharmony_ci    def calltip_show(event):
1867db96d56Sopenharmony_ci        calltip.showtip("(s='Hello world')", "insert", "end")
1877db96d56Sopenharmony_ci    def calltip_hide(event):
1887db96d56Sopenharmony_ci        calltip.hidetip()
1897db96d56Sopenharmony_ci    text.event_add("<<calltip-show>>", "(")
1907db96d56Sopenharmony_ci    text.event_add("<<calltip-hide>>", ")")
1917db96d56Sopenharmony_ci    text.bind("<<calltip-show>>", calltip_show)
1927db96d56Sopenharmony_ci    text.bind("<<calltip-hide>>", calltip_hide)
1937db96d56Sopenharmony_ci
1947db96d56Sopenharmony_ci    text.focus_set()
1957db96d56Sopenharmony_ci
1967db96d56Sopenharmony_ciif __name__ == '__main__':
1977db96d56Sopenharmony_ci    from unittest import main
1987db96d56Sopenharmony_ci    main('idlelib.idle_test.test_calltip_w', verbosity=2, exit=False)
1997db96d56Sopenharmony_ci
2007db96d56Sopenharmony_ci    from idlelib.idle_test.htest import run
2017db96d56Sopenharmony_ci    run(_calltip_window)
202