Commit 87e59ac1 authored by Tal Einat's avatar Tal Einat Committed by GitHub

bpo-33839: refactor IDLE's tooltips & calltips, add docstrings and tests (GH-7683)

* make CallTip and ToolTip sub-classes of a common abstract base class
* remove ListboxToolTip (unused and ugly)
* greatly increase test coverage
* tested on Windows, Linux and macOS
parent 2e5566d9
...@@ -51,7 +51,7 @@ class Calltip: ...@@ -51,7 +51,7 @@ class Calltip:
self.open_calltip(False) self.open_calltip(False)
def refresh_calltip_event(self, event): def refresh_calltip_event(self, event):
if self.active_calltip and self.active_calltip.is_active(): if self.active_calltip and self.active_calltip.tipwindow:
self.open_calltip(False) self.open_calltip(False)
def open_calltip(self, evalfuncs): def open_calltip(self, evalfuncs):
......
"""A calltip window class for Tkinter/IDLE. """A call-tip window class for Tkinter/IDLE.
After tooltip.py, which uses ideas gleaned from PySol After tooltip.py, which uses ideas gleaned from PySol.
Used by calltip. Used by calltip.py.
""" """
from tkinter import Toplevel, Label, LEFT, SOLID, TclError from tkinter import Label, LEFT, SOLID, TclError
HIDE_VIRTUAL_EVENT_NAME = "<<calltipwindow-hide>>" from idlelib.tooltip import TooltipBase
HIDE_EVENT = "<<calltipwindow-hide>>"
HIDE_SEQUENCES = ("<Key-Escape>", "<FocusOut>") HIDE_SEQUENCES = ("<Key-Escape>", "<FocusOut>")
CHECKHIDE_VIRTUAL_EVENT_NAME = "<<calltipwindow-checkhide>>" CHECKHIDE_EVENT = "<<calltipwindow-checkhide>>"
CHECKHIDE_SEQUENCES = ("<KeyRelease>", "<ButtonRelease>") CHECKHIDE_SEQUENCES = ("<KeyRelease>", "<ButtonRelease>")
CHECKHIDE_TIME = 100 # milliseconds CHECKHIDE_TIME = 100 # milliseconds
MARK_RIGHT = "calltipwindowregion_right" MARK_RIGHT = "calltipwindowregion_right"
class CalltipWindow:
def __init__(self, widget): class CalltipWindow(TooltipBase):
self.widget = widget """A call-tip widget for tkinter text widgets."""
self.tipwindow = self.label = None
self.parenline = self.parencol = None def __init__(self, text_widget):
self.lastline = None """Create a call-tip; shown by showtip().
text_widget: a Text widget with code for which call-tips are desired
"""
# Note: The Text widget will be accessible as self.anchor_widget
super(CalltipWindow, self).__init__(text_widget)
self.label = self.text = None
self.parenline = self.parencol = self.lastline = None
self.hideid = self.checkhideid = None self.hideid = self.checkhideid = None
self.checkhide_after_id = None self.checkhide_after_id = None
def position_window(self): def get_position(self):
"""Check if needs to reposition the window, and if so - do it.""" """Choose the position of the call-tip."""
curline = int(self.widget.index("insert").split('.')[0]) curline = int(self.anchor_widget.index("insert").split('.')[0])
if curline == self.lastline:
return
self.lastline = curline
self.widget.see("insert")
if curline == self.parenline: if curline == self.parenline:
box = self.widget.bbox("%d.%d" % (self.parenline, anchor_index = (self.parenline, self.parencol)
self.parencol))
else: else:
box = self.widget.bbox("%d.0" % curline) anchor_index = (curline, 0)
box = self.anchor_widget.bbox("%d.%d" % anchor_index)
if not box: if not box:
box = list(self.widget.bbox("insert")) box = list(self.anchor_widget.bbox("insert"))
# align to left of window # align to left of window
box[0] = 0 box[0] = 0
box[2] = 0 box[2] = 0
x = box[0] + self.widget.winfo_rootx() + 2 return box[0] + 2, box[1] + box[3]
y = box[1] + box[3] + self.widget.winfo_rooty()
self.tipwindow.wm_geometry("+%d+%d" % (x, y)) def position_window(self):
"Reposition the window if needed."
curline = int(self.anchor_widget.index("insert").split('.')[0])
if curline == self.lastline:
return
self.lastline = curline
self.anchor_widget.see("insert")
super(CalltipWindow, self).position_window()
def showtip(self, text, parenleft, parenright): def showtip(self, text, parenleft, parenright):
"""Show the calltip, bind events which will close it and reposition it. """Show the call-tip, bind events which will close it and reposition it.
text: the text to display in the call-tip
parenleft: index of the opening parenthesis in the text widget
parenright: index of the closing parenthesis in the text widget,
or the end of the line if there is no closing parenthesis
""" """
# Only called in calltip.Calltip, where lines are truncated # Only called in calltip.Calltip, where lines are truncated
self.text = text self.text = text
if self.tipwindow or not self.text: if self.tipwindow or not self.text:
return return
self.widget.mark_set(MARK_RIGHT, parenright) self.anchor_widget.mark_set(MARK_RIGHT, parenright)
self.parenline, self.parencol = map( self.parenline, self.parencol = map(
int, self.widget.index(parenleft).split(".")) int, self.anchor_widget.index(parenleft).split("."))
self.tipwindow = tw = Toplevel(self.widget) super(CalltipWindow, self).showtip()
self.position_window()
# remove border on calltip window self._bind_events()
tw.wm_overrideredirect(1)
try: def showcontents(self):
# This command is only needed and available on Tk >= 8.4.0 for OSX """Create the call-tip widget."""
# Without it, call tips intrude on the typing process by grabbing self.label = Label(self.tipwindow, text=self.text, justify=LEFT,
# the focus.
tw.tk.call("::tk::unsupported::MacWindowStyle", "style", tw._w,
"help", "noActivates")
except TclError:
pass
self.label = Label(tw, text=self.text, justify=LEFT,
background="#ffffe0", relief=SOLID, borderwidth=1, background="#ffffe0", relief=SOLID, borderwidth=1,
font = self.widget['font']) font=self.anchor_widget['font'])
self.label.pack() self.label.pack()
tw.update_idletasks()
tw.lift() # work around bug in Tk 8.5.18+ (issue #24570)
self.checkhideid = self.widget.bind(CHECKHIDE_VIRTUAL_EVENT_NAME,
self.checkhide_event)
for seq in CHECKHIDE_SEQUENCES:
self.widget.event_add(CHECKHIDE_VIRTUAL_EVENT_NAME, seq)
self.widget.after(CHECKHIDE_TIME, self.checkhide_event)
self.hideid = self.widget.bind(HIDE_VIRTUAL_EVENT_NAME,
self.hide_event)
for seq in HIDE_SEQUENCES:
self.widget.event_add(HIDE_VIRTUAL_EVENT_NAME, seq)
def checkhide_event(self, event=None): def checkhide_event(self, event=None):
"""Handle CHECK_HIDE_EVENT: call hidetip or reschedule."""
if not self.tipwindow: if not self.tipwindow:
# If the event was triggered by the same event that unbinded # If the event was triggered by the same event that unbound
# this function, the function will be called nevertheless, # this function, the function will be called nevertheless,
# so do nothing in this case. # so do nothing in this case.
return None return None
curline, curcol = map(int, self.widget.index("insert").split('.'))
# Hide the call-tip if the insertion cursor moves outside of the
# parenthesis.
curline, curcol = map(int, self.anchor_widget.index("insert").split('.'))
if curline < self.parenline or \ if curline < self.parenline or \
(curline == self.parenline and curcol <= self.parencol) or \ (curline == self.parenline and curcol <= self.parencol) or \
self.widget.compare("insert", ">", MARK_RIGHT): self.anchor_widget.compare("insert", ">", MARK_RIGHT):
self.hidetip() self.hidetip()
return "break" return "break"
else:
# Not hiding the call-tip.
self.position_window() self.position_window()
# Re-schedule this function to be called again in a short while.
if self.checkhide_after_id is not None: if self.checkhide_after_id is not None:
self.widget.after_cancel(self.checkhide_after_id) self.anchor_widget.after_cancel(self.checkhide_after_id)
self.checkhide_after_id = \ self.checkhide_after_id = \
self.widget.after(CHECKHIDE_TIME, self.checkhide_event) self.anchor_widget.after(CHECKHIDE_TIME, self.checkhide_event)
return None return None
def hide_event(self, event): def hide_event(self, event):
"""Handle HIDE_EVENT by calling hidetip."""
if not self.tipwindow: if not self.tipwindow:
# See the explanation in checkhide_event. # See the explanation in checkhide_event.
return None return None
...@@ -113,51 +120,76 @@ class CalltipWindow: ...@@ -113,51 +120,76 @@ class CalltipWindow:
return "break" return "break"
def hidetip(self): def hidetip(self):
"""Hide the call-tip."""
if not self.tipwindow: if not self.tipwindow:
return return
for seq in CHECKHIDE_SEQUENCES: try:
self.widget.event_delete(CHECKHIDE_VIRTUAL_EVENT_NAME, seq)
self.widget.unbind(CHECKHIDE_VIRTUAL_EVENT_NAME, self.checkhideid)
self.checkhideid = None
for seq in HIDE_SEQUENCES:
self.widget.event_delete(HIDE_VIRTUAL_EVENT_NAME, seq)
self.widget.unbind(HIDE_VIRTUAL_EVENT_NAME, self.hideid)
self.hideid = None
self.label.destroy() self.label.destroy()
except TclError:
pass
self.label = None self.label = None
self.tipwindow.destroy()
self.tipwindow = None
self.widget.mark_unset(MARK_RIGHT)
self.parenline = self.parencol = self.lastline = None self.parenline = self.parencol = self.lastline = None
try:
self.anchor_widget.mark_unset(MARK_RIGHT)
except TclError:
pass
def is_active(self): try:
return bool(self.tipwindow) self._unbind_events()
except (TclError, ValueError):
# ValueError may be raised by MultiCall
pass
super(CalltipWindow, self).hidetip()
def _bind_events(self):
"""Bind event handlers."""
self.checkhideid = self.anchor_widget.bind(CHECKHIDE_EVENT,
self.checkhide_event)
for seq in CHECKHIDE_SEQUENCES:
self.anchor_widget.event_add(CHECKHIDE_EVENT, seq)
self.anchor_widget.after(CHECKHIDE_TIME, self.checkhide_event)
self.hideid = self.anchor_widget.bind(HIDE_EVENT,
self.hide_event)
for seq in HIDE_SEQUENCES:
self.anchor_widget.event_add(HIDE_EVENT, seq)
def _unbind_events(self):
"""Unbind event handlers."""
for seq in CHECKHIDE_SEQUENCES:
self.anchor_widget.event_delete(CHECKHIDE_EVENT, seq)
self.anchor_widget.unbind(CHECKHIDE_EVENT, self.checkhideid)
self.checkhideid = None
for seq in HIDE_SEQUENCES:
self.anchor_widget.event_delete(HIDE_EVENT, seq)
self.anchor_widget.unbind(HIDE_EVENT, self.hideid)
self.hideid = None
def _calltip_window(parent): # htest # def _calltip_window(parent): # htest #
from tkinter import Toplevel, Text, LEFT, BOTH from tkinter import Toplevel, Text, LEFT, BOTH
top = Toplevel(parent) top = Toplevel(parent)
top.title("Test calltips") top.title("Test call-tips")
x, y = map(int, parent.geometry().split('+')[1:]) x, y = map(int, parent.geometry().split('+')[1:])
top.geometry("200x100+%d+%d" % (x + 250, y + 175)) top.geometry("250x100+%d+%d" % (x + 175, y + 150))
text = Text(top) text = Text(top)
text.pack(side=LEFT, fill=BOTH, expand=1) text.pack(side=LEFT, fill=BOTH, expand=1)
text.insert("insert", "string.split") text.insert("insert", "string.split")
top.update() top.update()
calltip = CalltipWindow(text)
calltip = CalltipWindow(text)
def calltip_show(event): def calltip_show(event):
calltip.showtip("(s=Hello world)", "insert", "end") calltip.showtip("(s='Hello world')", "insert", "end")
def calltip_hide(event): def calltip_hide(event):
calltip.hidetip() calltip.hidetip()
text.event_add("<<calltip-show>>", "(") text.event_add("<<calltip-show>>", "(")
text.event_add("<<calltip-hide>>", ")") text.event_add("<<calltip-hide>>", ")")
text.bind("<<calltip-show>>", calltip_show) text.bind("<<calltip-show>>", calltip_show)
text.bind("<<calltip-hide>>", calltip_hide) text.bind("<<calltip-hide>>", calltip_hide)
text.focus_set() text.focus_set()
if __name__ == '__main__': if __name__ == '__main__':
......
...@@ -80,11 +80,14 @@ AboutDialog_spec = { ...@@ -80,11 +80,14 @@ AboutDialog_spec = {
"are correctly displayed.\n [Close] to exit.", "are correctly displayed.\n [Close] to exit.",
} }
# TODO implement ^\; adding '<Control-Key-\\>' to function does not work.
_calltip_window_spec = { _calltip_window_spec = {
'file': 'calltip_w', 'file': 'calltip_w',
'kwds': {}, 'kwds': {},
'msg': "Typing '(' should display a calltip.\n" 'msg': "Typing '(' should display a calltip.\n"
"Typing ') should hide the calltip.\n" "Typing ') should hide the calltip.\n"
"So should moving cursor out of argument area.\n"
"Force-open-calltip does not work here.\n"
} }
_module_browser_spec = { _module_browser_spec = {
......
...@@ -23,7 +23,7 @@ class CallTipWindowTest(unittest.TestCase): ...@@ -23,7 +23,7 @@ class CallTipWindowTest(unittest.TestCase):
del cls.text, cls.root del cls.text, cls.root
def test_init(self): def test_init(self):
self.assertEqual(self.calltip.widget, self.text) self.assertEqual(self.calltip.anchor_widget, self.text)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main(verbosity=2) unittest.main(verbosity=2)
from idlelib.tooltip import TooltipBase, Hovertip
from test.support import requires
requires('gui')
from functools import wraps
import time
from tkinter import Button, Tk, Toplevel
import unittest
def setUpModule():
global root
root = Tk()
def root_update():
global root
root.update()
def tearDownModule():
global root
root.update_idletasks()
root.destroy()
del root
def add_call_counting(func):
@wraps(func)
def wrapped_func(*args, **kwargs):
wrapped_func.call_args_list.append((args, kwargs))
return func(*args, **kwargs)
wrapped_func.call_args_list = []
return wrapped_func
def _make_top_and_button(testobj):
global root
top = Toplevel(root)
testobj.addCleanup(top.destroy)
top.title("Test tooltip")
button = Button(top, text='ToolTip test button')
button.pack()
testobj.addCleanup(button.destroy)
top.lift()
return top, button
class ToolTipBaseTest(unittest.TestCase):
def setUp(self):
self.top, self.button = _make_top_and_button(self)
def test_base_class_is_unusable(self):
global root
top = Toplevel(root)
self.addCleanup(top.destroy)
button = Button(top, text='ToolTip test button')
button.pack()
self.addCleanup(button.destroy)
with self.assertRaises(NotImplementedError):
tooltip = TooltipBase(button)
tooltip.showtip()
class HovertipTest(unittest.TestCase):
def setUp(self):
self.top, self.button = _make_top_and_button(self)
def test_showtip(self):
tooltip = Hovertip(self.button, 'ToolTip text')
self.addCleanup(tooltip.hidetip)
self.assertFalse(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable())
tooltip.showtip()
self.assertTrue(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable())
def test_showtip_twice(self):
tooltip = Hovertip(self.button, 'ToolTip text')
self.addCleanup(tooltip.hidetip)
self.assertFalse(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable())
tooltip.showtip()
self.assertTrue(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable())
orig_tipwindow = tooltip.tipwindow
tooltip.showtip()
self.assertTrue(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable())
self.assertIs(tooltip.tipwindow, orig_tipwindow)
def test_hidetip(self):
tooltip = Hovertip(self.button, 'ToolTip text')
self.addCleanup(tooltip.hidetip)
tooltip.showtip()
tooltip.hidetip()
self.assertFalse(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable())
def test_showtip_on_mouse_enter_no_delay(self):
tooltip = Hovertip(self.button, 'ToolTip text', hover_delay=None)
self.addCleanup(tooltip.hidetip)
tooltip.showtip = add_call_counting(tooltip.showtip)
root_update()
self.assertFalse(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable())
self.button.event_generate('<Enter>', x=0, y=0)
root_update()
self.assertTrue(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable())
self.assertGreater(len(tooltip.showtip.call_args_list), 0)
def test_showtip_on_mouse_enter_hover_delay(self):
tooltip = Hovertip(self.button, 'ToolTip text', hover_delay=50)
self.addCleanup(tooltip.hidetip)
tooltip.showtip = add_call_counting(tooltip.showtip)
root_update()
self.assertFalse(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable())
self.button.event_generate('<Enter>', x=0, y=0)
root_update()
self.assertFalse(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable())
time.sleep(0.1)
root_update()
self.assertTrue(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable())
self.assertGreater(len(tooltip.showtip.call_args_list), 0)
def test_hidetip_on_mouse_leave(self):
tooltip = Hovertip(self.button, 'ToolTip text', hover_delay=None)
self.addCleanup(tooltip.hidetip)
tooltip.showtip = add_call_counting(tooltip.showtip)
root_update()
self.button.event_generate('<Enter>', x=0, y=0)
root_update()
self.button.event_generate('<Leave>', x=0, y=0)
root_update()
self.assertFalse(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable())
self.assertGreater(len(tooltip.showtip.call_args_list), 0)
def test_dont_show_on_mouse_leave_before_delay(self):
tooltip = Hovertip(self.button, 'ToolTip text', hover_delay=50)
self.addCleanup(tooltip.hidetip)
tooltip.showtip = add_call_counting(tooltip.showtip)
root_update()
self.button.event_generate('<Enter>', x=0, y=0)
root_update()
self.button.event_generate('<Leave>', x=0, y=0)
root_update()
time.sleep(0.1)
root_update()
self.assertFalse(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable())
self.assertEqual(tooltip.showtip.call_args_list, [])
if __name__ == '__main__':
unittest.main(verbosity=2)
# general purpose 'tooltip' routines - currently unused in idlelib """Tools for displaying tool-tips.
# (although the 'calltips' extension is partly based on this code)
# may be useful for some purposes in (or almost in ;) the current project scope
# Ideas gleaned from PySol
This includes:
* an abstract base-class for different kinds of tooltips
* a simple text-only Tooltip class
"""
from tkinter import * from tkinter import *
class ToolTipBase:
def __init__(self, button): class TooltipBase(object):
self.button = button """abstract base class for tooltips"""
self.tipwindow = None
self.id = None
self.x = self.y = 0
self._id1 = self.button.bind("<Enter>", self.enter)
self._id2 = self.button.bind("<Leave>", self.leave)
self._id3 = self.button.bind("<ButtonPress>", self.leave)
def enter(self, event=None): def __init__(self, anchor_widget):
self.schedule() """Create a tooltip.
def leave(self, event=None): anchor_widget: the widget next to which the tooltip will be shown
self.unschedule()
self.hidetip()
def schedule(self): Note that a widget will only be shown when showtip() is called.
self.unschedule() """
self.id = self.button.after(1500, self.showtip) self.anchor_widget = anchor_widget
self.tipwindow = None
def unschedule(self): def __del__(self):
id = self.id self.hidetip()
self.id = None
if id:
self.button.after_cancel(id)
def showtip(self): def showtip(self):
"""display the tooltip"""
if self.tipwindow: if self.tipwindow:
return return
# The tip window must be completely outside the button; self.tipwindow = tw = Toplevel(self.anchor_widget)
# show no border on the top level window
tw.wm_overrideredirect(1)
try:
# This command is only needed and available on Tk >= 8.4.0 for OSX.
# Without it, call tips intrude on the typing process by grabbing
# the focus.
tw.tk.call("::tk::unsupported::MacWindowStyle", "style", tw._w,
"help", "noActivates")
except TclError:
pass
self.position_window()
self.showcontents()
self.tipwindow.update_idletasks() # Needed on MacOS -- see #34275.
self.tipwindow.lift() # work around bug in Tk 8.5.18+ (issue #24570)
def position_window(self):
"""(re)-set the tooltip's screen position"""
x, y = self.get_position()
root_x = self.anchor_widget.winfo_rootx() + x
root_y = self.anchor_widget.winfo_rooty() + y
self.tipwindow.wm_geometry("+%d+%d" % (root_x, root_y))
def get_position(self):
"""choose a screen position for the tooltip"""
# The tip window must be completely outside the anchor widget;
# otherwise when the mouse enters the tip window we get # otherwise when the mouse enters the tip window we get
# a leave event and it disappears, and then we get an enter # a leave event and it disappears, and then we get an enter
# event and it reappears, and so on forever :-( # event and it reappears, and so on forever :-(
x = self.button.winfo_rootx() + 20 #
y = self.button.winfo_rooty() + self.button.winfo_height() + 1 # Note: This is a simplistic implementation; sub-classes will likely
self.tipwindow = tw = Toplevel(self.button) # want to override this.
tw.wm_overrideredirect(1) return 20, self.anchor_widget.winfo_height() + 1
tw.wm_geometry("+%d+%d" % (x, y))
self.showcontents()
def showcontents(self, text="Your text here"): def showcontents(self):
# Override this in derived class """content display hook for sub-classes"""
label = Label(self.tipwindow, text=text, justify=LEFT, # See ToolTip for an example
background="#ffffe0", relief=SOLID, borderwidth=1) raise NotImplementedError
label.pack()
def hidetip(self): def hidetip(self):
"""hide the tooltip"""
# Note: This is called by __del__, so careful when overriding/extending
tw = self.tipwindow tw = self.tipwindow
self.tipwindow = None self.tipwindow = None
if tw: if tw:
try:
tw.destroy() tw.destroy()
except TclError:
pass
class OnHoverTooltipBase(TooltipBase):
"""abstract base class for tooltips, with delayed on-hover display"""
def __init__(self, anchor_widget, hover_delay=1000):
"""Create a tooltip with a mouse hover delay.
anchor_widget: the widget next to which the tooltip will be shown
hover_delay: time to delay before showing the tooltip, in milliseconds
Note that a widget will only be shown when showtip() is called,
e.g. after hovering over the anchor widget with the mouse for enough
time.
"""
super(OnHoverTooltipBase, self).__init__(anchor_widget)
self.hover_delay = hover_delay
class ToolTip(ToolTipBase): self._after_id = None
def __init__(self, button, text): self._id1 = self.anchor_widget.bind("<Enter>", self._show_event)
ToolTipBase.__init__(self, button) self._id2 = self.anchor_widget.bind("<Leave>", self._hide_event)
self._id3 = self.anchor_widget.bind("<Button>", self._hide_event)
def __del__(self):
try:
self.anchor_widget.unbind("<Enter>", self._id1)
self.anchor_widget.unbind("<Leave>", self._id2)
self.anchor_widget.unbind("<Button>", self._id3)
except TclError:
pass
super(OnHoverTooltipBase, self).__del__()
def _show_event(self, event=None):
"""event handler to display the tooltip"""
if self.hover_delay:
self.schedule()
else:
self.showtip()
def _hide_event(self, event=None):
"""event handler to hide the tooltip"""
self.hidetip()
def schedule(self):
"""schedule the future display of the tooltip"""
self.unschedule()
self._after_id = self.anchor_widget.after(self.hover_delay,
self.showtip)
def unschedule(self):
"""cancel the future display of the tooltip"""
after_id = self._after_id
self._after_id = None
if after_id:
self.anchor_widget.after_cancel(after_id)
def hidetip(self):
"""hide the tooltip"""
try:
self.unschedule()
except TclError:
pass
super(OnHoverTooltipBase, self).hidetip()
class Hovertip(OnHoverTooltipBase):
"A tooltip that pops up when a mouse hovers over an anchor widget."
def __init__(self, anchor_widget, text, hover_delay=1000):
"""Create a text tooltip with a mouse hover delay.
anchor_widget: the widget next to which the tooltip will be shown
hover_delay: time to delay before showing the tooltip, in milliseconds
Note that a widget will only be shown when showtip() is called,
e.g. after hovering over the anchor widget with the mouse for enough
time.
"""
super(Hovertip, self).__init__(anchor_widget, hover_delay=hover_delay)
self.text = text self.text = text
def showcontents(self):
ToolTipBase.showcontents(self, self.text)
class ListboxToolTip(ToolTipBase):
def __init__(self, button, items):
ToolTipBase.__init__(self, button)
self.items = items
def showcontents(self): def showcontents(self):
listbox = Listbox(self.tipwindow, background="#ffffe0") label = Label(self.tipwindow, text=self.text, justify=LEFT,
listbox.pack() background="#ffffe0", relief=SOLID, borderwidth=1)
for item in self.items: label.pack()
listbox.insert(END, item)
def _tooltip(parent): # htest # def _tooltip(parent): # htest #
top = Toplevel(parent) top = Toplevel(parent)
...@@ -83,14 +170,17 @@ def _tooltip(parent): # htest # ...@@ -83,14 +170,17 @@ def _tooltip(parent): # htest #
top.geometry("+%d+%d" % (x, y + 150)) top.geometry("+%d+%d" % (x, y + 150))
label = Label(top, text="Place your mouse over buttons") label = Label(top, text="Place your mouse over buttons")
label.pack() label.pack()
button1 = Button(top, text="Button 1") button1 = Button(top, text="Button 1 -- 1/2 second hover delay")
button2 = Button(top, text="Button 2")
button1.pack() button1.pack()
Hovertip(button1, "This is tooltip text for button1.", hover_delay=500)
button2 = Button(top, text="Button 2 -- no hover delay")
button2.pack() button2.pack()
ToolTip(button1, "This is tooltip text for button1.") Hovertip(button2, "This is tooltip\ntext for button2.", hover_delay=None)
ListboxToolTip(button2, ["This is","multiple line",
"tooltip text","for button2"])
if __name__ == '__main__': if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_tooltip', verbosity=2, exit=False)
from idlelib.idle_test.htest import run from idlelib.idle_test.htest import run
run(_tooltip) run(_tooltip)
IDLE: refactor ToolTip and CallTip and add documentation and tests
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment