17db96d56Sopenharmony_ci"""Tools for displaying tool-tips.
27db96d56Sopenharmony_ci
37db96d56Sopenharmony_ciThis includes:
47db96d56Sopenharmony_ci * an abstract base-class for different kinds of tooltips
57db96d56Sopenharmony_ci * a simple text-only Tooltip class
67db96d56Sopenharmony_ci"""
77db96d56Sopenharmony_cifrom tkinter import *
87db96d56Sopenharmony_ci
97db96d56Sopenharmony_ci
107db96d56Sopenharmony_ciclass TooltipBase:
117db96d56Sopenharmony_ci    """abstract base class for tooltips"""
127db96d56Sopenharmony_ci
137db96d56Sopenharmony_ci    def __init__(self, anchor_widget):
147db96d56Sopenharmony_ci        """Create a tooltip.
157db96d56Sopenharmony_ci
167db96d56Sopenharmony_ci        anchor_widget: the widget next to which the tooltip will be shown
177db96d56Sopenharmony_ci
187db96d56Sopenharmony_ci        Note that a widget will only be shown when showtip() is called.
197db96d56Sopenharmony_ci        """
207db96d56Sopenharmony_ci        self.anchor_widget = anchor_widget
217db96d56Sopenharmony_ci        self.tipwindow = None
227db96d56Sopenharmony_ci
237db96d56Sopenharmony_ci    def __del__(self):
247db96d56Sopenharmony_ci        self.hidetip()
257db96d56Sopenharmony_ci
267db96d56Sopenharmony_ci    def showtip(self):
277db96d56Sopenharmony_ci        """display the tooltip"""
287db96d56Sopenharmony_ci        if self.tipwindow:
297db96d56Sopenharmony_ci            return
307db96d56Sopenharmony_ci        self.tipwindow = tw = Toplevel(self.anchor_widget)
317db96d56Sopenharmony_ci        # show no border on the top level window
327db96d56Sopenharmony_ci        tw.wm_overrideredirect(1)
337db96d56Sopenharmony_ci        try:
347db96d56Sopenharmony_ci            # This command is only needed and available on Tk >= 8.4.0 for OSX.
357db96d56Sopenharmony_ci            # Without it, call tips intrude on the typing process by grabbing
367db96d56Sopenharmony_ci            # the focus.
377db96d56Sopenharmony_ci            tw.tk.call("::tk::unsupported::MacWindowStyle", "style", tw._w,
387db96d56Sopenharmony_ci                       "help", "noActivates")
397db96d56Sopenharmony_ci        except TclError:
407db96d56Sopenharmony_ci            pass
417db96d56Sopenharmony_ci
427db96d56Sopenharmony_ci        self.position_window()
437db96d56Sopenharmony_ci        self.showcontents()
447db96d56Sopenharmony_ci        self.tipwindow.update_idletasks()  # Needed on MacOS -- see #34275.
457db96d56Sopenharmony_ci        self.tipwindow.lift()  # work around bug in Tk 8.5.18+ (issue #24570)
467db96d56Sopenharmony_ci
477db96d56Sopenharmony_ci    def position_window(self):
487db96d56Sopenharmony_ci        """(re)-set the tooltip's screen position"""
497db96d56Sopenharmony_ci        x, y = self.get_position()
507db96d56Sopenharmony_ci        root_x = self.anchor_widget.winfo_rootx() + x
517db96d56Sopenharmony_ci        root_y = self.anchor_widget.winfo_rooty() + y
527db96d56Sopenharmony_ci        self.tipwindow.wm_geometry("+%d+%d" % (root_x, root_y))
537db96d56Sopenharmony_ci
547db96d56Sopenharmony_ci    def get_position(self):
557db96d56Sopenharmony_ci        """choose a screen position for the tooltip"""
567db96d56Sopenharmony_ci        # The tip window must be completely outside the anchor widget;
577db96d56Sopenharmony_ci        # otherwise when the mouse enters the tip window we get
587db96d56Sopenharmony_ci        # a leave event and it disappears, and then we get an enter
597db96d56Sopenharmony_ci        # event and it reappears, and so on forever :-(
607db96d56Sopenharmony_ci        #
617db96d56Sopenharmony_ci        # Note: This is a simplistic implementation; sub-classes will likely
627db96d56Sopenharmony_ci        # want to override this.
637db96d56Sopenharmony_ci        return 20, self.anchor_widget.winfo_height() + 1
647db96d56Sopenharmony_ci
657db96d56Sopenharmony_ci    def showcontents(self):
667db96d56Sopenharmony_ci        """content display hook for sub-classes"""
677db96d56Sopenharmony_ci        # See ToolTip for an example
687db96d56Sopenharmony_ci        raise NotImplementedError
697db96d56Sopenharmony_ci
707db96d56Sopenharmony_ci    def hidetip(self):
717db96d56Sopenharmony_ci        """hide the tooltip"""
727db96d56Sopenharmony_ci        # Note: This is called by __del__, so careful when overriding/extending
737db96d56Sopenharmony_ci        tw = self.tipwindow
747db96d56Sopenharmony_ci        self.tipwindow = None
757db96d56Sopenharmony_ci        if tw:
767db96d56Sopenharmony_ci            try:
777db96d56Sopenharmony_ci                tw.destroy()
787db96d56Sopenharmony_ci            except TclError:  # pragma: no cover
797db96d56Sopenharmony_ci                pass
807db96d56Sopenharmony_ci
817db96d56Sopenharmony_ci
827db96d56Sopenharmony_ciclass OnHoverTooltipBase(TooltipBase):
837db96d56Sopenharmony_ci    """abstract base class for tooltips, with delayed on-hover display"""
847db96d56Sopenharmony_ci
857db96d56Sopenharmony_ci    def __init__(self, anchor_widget, hover_delay=1000):
867db96d56Sopenharmony_ci        """Create a tooltip with a mouse hover delay.
877db96d56Sopenharmony_ci
887db96d56Sopenharmony_ci        anchor_widget: the widget next to which the tooltip will be shown
897db96d56Sopenharmony_ci        hover_delay: time to delay before showing the tooltip, in milliseconds
907db96d56Sopenharmony_ci
917db96d56Sopenharmony_ci        Note that a widget will only be shown when showtip() is called,
927db96d56Sopenharmony_ci        e.g. after hovering over the anchor widget with the mouse for enough
937db96d56Sopenharmony_ci        time.
947db96d56Sopenharmony_ci        """
957db96d56Sopenharmony_ci        super().__init__(anchor_widget)
967db96d56Sopenharmony_ci        self.hover_delay = hover_delay
977db96d56Sopenharmony_ci
987db96d56Sopenharmony_ci        self._after_id = None
997db96d56Sopenharmony_ci        self._id1 = self.anchor_widget.bind("<Enter>", self._show_event)
1007db96d56Sopenharmony_ci        self._id2 = self.anchor_widget.bind("<Leave>", self._hide_event)
1017db96d56Sopenharmony_ci        self._id3 = self.anchor_widget.bind("<Button>", self._hide_event)
1027db96d56Sopenharmony_ci
1037db96d56Sopenharmony_ci    def __del__(self):
1047db96d56Sopenharmony_ci        try:
1057db96d56Sopenharmony_ci            self.anchor_widget.unbind("<Enter>", self._id1)
1067db96d56Sopenharmony_ci            self.anchor_widget.unbind("<Leave>", self._id2)  # pragma: no cover
1077db96d56Sopenharmony_ci            self.anchor_widget.unbind("<Button>", self._id3) # pragma: no cover
1087db96d56Sopenharmony_ci        except TclError:
1097db96d56Sopenharmony_ci            pass
1107db96d56Sopenharmony_ci        super().__del__()
1117db96d56Sopenharmony_ci
1127db96d56Sopenharmony_ci    def _show_event(self, event=None):
1137db96d56Sopenharmony_ci        """event handler to display the tooltip"""
1147db96d56Sopenharmony_ci        if self.hover_delay:
1157db96d56Sopenharmony_ci            self.schedule()
1167db96d56Sopenharmony_ci        else:
1177db96d56Sopenharmony_ci            self.showtip()
1187db96d56Sopenharmony_ci
1197db96d56Sopenharmony_ci    def _hide_event(self, event=None):
1207db96d56Sopenharmony_ci        """event handler to hide the tooltip"""
1217db96d56Sopenharmony_ci        self.hidetip()
1227db96d56Sopenharmony_ci
1237db96d56Sopenharmony_ci    def schedule(self):
1247db96d56Sopenharmony_ci        """schedule the future display of the tooltip"""
1257db96d56Sopenharmony_ci        self.unschedule()
1267db96d56Sopenharmony_ci        self._after_id = self.anchor_widget.after(self.hover_delay,
1277db96d56Sopenharmony_ci                                                  self.showtip)
1287db96d56Sopenharmony_ci
1297db96d56Sopenharmony_ci    def unschedule(self):
1307db96d56Sopenharmony_ci        """cancel the future display of the tooltip"""
1317db96d56Sopenharmony_ci        after_id = self._after_id
1327db96d56Sopenharmony_ci        self._after_id = None
1337db96d56Sopenharmony_ci        if after_id:
1347db96d56Sopenharmony_ci            self.anchor_widget.after_cancel(after_id)
1357db96d56Sopenharmony_ci
1367db96d56Sopenharmony_ci    def hidetip(self):
1377db96d56Sopenharmony_ci        """hide the tooltip"""
1387db96d56Sopenharmony_ci        try:
1397db96d56Sopenharmony_ci            self.unschedule()
1407db96d56Sopenharmony_ci        except TclError:  # pragma: no cover
1417db96d56Sopenharmony_ci            pass
1427db96d56Sopenharmony_ci        super().hidetip()
1437db96d56Sopenharmony_ci
1447db96d56Sopenharmony_ci
1457db96d56Sopenharmony_ciclass Hovertip(OnHoverTooltipBase):
1467db96d56Sopenharmony_ci    "A tooltip that pops up when a mouse hovers over an anchor widget."
1477db96d56Sopenharmony_ci    def __init__(self, anchor_widget, text, hover_delay=1000):
1487db96d56Sopenharmony_ci        """Create a text tooltip with a mouse hover delay.
1497db96d56Sopenharmony_ci
1507db96d56Sopenharmony_ci        anchor_widget: the widget next to which the tooltip will be shown
1517db96d56Sopenharmony_ci        hover_delay: time to delay before showing the tooltip, in milliseconds
1527db96d56Sopenharmony_ci
1537db96d56Sopenharmony_ci        Note that a widget will only be shown when showtip() is called,
1547db96d56Sopenharmony_ci        e.g. after hovering over the anchor widget with the mouse for enough
1557db96d56Sopenharmony_ci        time.
1567db96d56Sopenharmony_ci        """
1577db96d56Sopenharmony_ci        super().__init__(anchor_widget, hover_delay=hover_delay)
1587db96d56Sopenharmony_ci        self.text = text
1597db96d56Sopenharmony_ci
1607db96d56Sopenharmony_ci    def showcontents(self):
1617db96d56Sopenharmony_ci        label = Label(self.tipwindow, text=self.text, justify=LEFT,
1627db96d56Sopenharmony_ci                      background="#ffffe0", relief=SOLID, borderwidth=1)
1637db96d56Sopenharmony_ci        label.pack()
1647db96d56Sopenharmony_ci
1657db96d56Sopenharmony_ci
1667db96d56Sopenharmony_cidef _tooltip(parent):  # htest #
1677db96d56Sopenharmony_ci    top = Toplevel(parent)
1687db96d56Sopenharmony_ci    top.title("Test tooltip")
1697db96d56Sopenharmony_ci    x, y = map(int, parent.geometry().split('+')[1:])
1707db96d56Sopenharmony_ci    top.geometry("+%d+%d" % (x, y + 150))
1717db96d56Sopenharmony_ci    label = Label(top, text="Place your mouse over buttons")
1727db96d56Sopenharmony_ci    label.pack()
1737db96d56Sopenharmony_ci    button1 = Button(top, text="Button 1 -- 1/2 second hover delay")
1747db96d56Sopenharmony_ci    button1.pack()
1757db96d56Sopenharmony_ci    Hovertip(button1, "This is tooltip text for button1.", hover_delay=500)
1767db96d56Sopenharmony_ci    button2 = Button(top, text="Button 2 -- no hover delay")
1777db96d56Sopenharmony_ci    button2.pack()
1787db96d56Sopenharmony_ci    Hovertip(button2, "This is tooltip\ntext for button2.", hover_delay=None)
1797db96d56Sopenharmony_ci
1807db96d56Sopenharmony_ci
1817db96d56Sopenharmony_ciif __name__ == '__main__':
1827db96d56Sopenharmony_ci    from unittest import main
1837db96d56Sopenharmony_ci    main('idlelib.idle_test.test_tooltip', verbosity=2, exit=False)
1847db96d56Sopenharmony_ci
1857db96d56Sopenharmony_ci    from idlelib.idle_test.htest import run
1867db96d56Sopenharmony_ci    run(_tooltip)
187