Skip to content

bpo-33839: IDLE tooltips.py: refactor and add docstrings and tests #7683

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Aug 5, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
41c1e43
bpo-33839: cleanup tooltip.py
taleinat Jun 12, 2018
b0e9a84
bpo-33839: refactor CallTip and ToolTip classes
taleinat Jun 13, 2018
263117c
bpo-33839: add doc-string and comments and rename a few variables
taleinat Jun 13, 2018
187e0e5
bpo-33839: avoid Tcl exceptions upon closing IDLE with a call-tip shown
taleinat Jun 13, 2018
6fb9ba9
bpo-33839: add automated tests for tooltip.py (92% coverage)
taleinat Jun 13, 2018
f84a2b9
bpo-33839: add another tooltip test and improve existing tests
taleinat Jun 13, 2018
ef721ca
bpo-33839: add NEWS entry
taleinat Jun 14, 2018
f2857dd
Merge remote-tracking branch 'origin/master' into pr_7683
terryjreedy Jun 17, 2018
eeed210
Change 'widget', changes to '_text_widget' in calltip_w, to 'test_wid…
terryjreedy Jun 17, 2018
5afa8e1
Quote string arg in htest.
terryjreedy Jun 17, 2018
1198a0c
Shorten HIDE_VIRTUAL_EVENT_NAME to HIDE_EVENT.
terryjreedy Jun 17, 2018
d9bcc1f
Add unittest invocation. This not only allows running test from editor,
terryjreedy Jun 17, 2018
bae3bb2
Improve message for calltip htest.
terryjreedy Jun 18, 2018
dd677b5
bpo-33839: change capitalization: "ToolTip" -> "Tooltip"
taleinat Jun 20, 2018
1c990f3
bpo-33839: add doc-strings for __init__ methods in tooltip.py
taleinat Jun 20, 2018
c55ca31
Merge remote-tracking branch 'upstream/master' into bpo-33839
taleinat Jun 20, 2018
a1b22e4
bpo-33839: remove duplication of .anchor_widget as .text_widget
taleinat Jun 20, 2018
3ecbd65
bpo-33839: improve doc-strings and comments plus minor code cleanup
taleinat Jun 22, 2018
a7f858b
bpo-33839: add a test for tooltip avoiding creating duplicate TopLevel
taleinat Jun 22, 2018
5d3c384
Center box; widen so title all visible.
terryjreedy Jun 29, 2018
f81fbbf
calltip_w docstrings
terryjreedy Jun 29, 2018
3847011
bpo-33839: remove Tooltip.is_active()
taleinat Jun 29, 2018
5d92ba0
Merge branch 'master' into bpo-33839
terryjreedy Aug 3, 2018
8adaa77
update_idletasks in show_tip, Texttip > Hovertip
terryjreedy Aug 3, 2018
23d57c8
Make tests pass with Hovertip.
terryjreedy Aug 4, 2018
98f4ae4
bpo-33839: document hover_delay parameter and fix indentation
taleinat Aug 5, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Lib/idlelib/calltip.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def try_open_calltip_event(self, event):
self.open_calltip(False)

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)

def open_calltip(self, evalfuncs):
Expand Down
202 changes: 117 additions & 85 deletions Lib/idlelib/calltip_w.py
Original file line number Diff line number Diff line change
@@ -1,163 +1,195 @@
"""A calltip window class for Tkinter/IDLE.
"""A call-tip window class for Tkinter/IDLE.

After tooltip.py, which uses ideas gleaned from PySol
Used by calltip.
After tooltip.py, which uses ideas gleaned from PySol.
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>")
CHECKHIDE_VIRTUAL_EVENT_NAME = "<<calltipwindow-checkhide>>"
CHECKHIDE_EVENT = "<<calltipwindow-checkhide>>"
CHECKHIDE_SEQUENCES = ("<KeyRelease>", "<ButtonRelease>")
CHECKHIDE_TIME = 100 # milliseconds
CHECKHIDE_TIME = 100 # milliseconds

MARK_RIGHT = "calltipwindowregion_right"

class CalltipWindow:

def __init__(self, widget):
self.widget = widget
self.tipwindow = self.label = None
self.parenline = self.parencol = None
self.lastline = None
class CalltipWindow(TooltipBase):
"""A call-tip widget for tkinter text widgets."""

def __init__(self, text_widget):
"""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.checkhide_after_id = None

def position_window(self):
"""Check if needs to reposition the window, and if so - do it."""
curline = int(self.widget.index("insert").split('.')[0])
if curline == self.lastline:
return
self.lastline = curline
self.widget.see("insert")
def get_position(self):
"""Choose the position of the call-tip."""
curline = int(self.anchor_widget.index("insert").split('.')[0])
if curline == self.parenline:
box = self.widget.bbox("%d.%d" % (self.parenline,
self.parencol))
anchor_index = (self.parenline, self.parencol)
else:
box = self.widget.bbox("%d.0" % curline)
anchor_index = (curline, 0)
box = self.anchor_widget.bbox("%d.%d" % anchor_index)
if not box:
box = list(self.widget.bbox("insert"))
box = list(self.anchor_widget.bbox("insert"))
# align to left of window
box[0] = 0
box[2] = 0
x = box[0] + self.widget.winfo_rootx() + 2
y = box[1] + box[3] + self.widget.winfo_rooty()
self.tipwindow.wm_geometry("+%d+%d" % (x, y))
return box[0] + 2, box[1] + box[3]

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):
"""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
self.text = text
if self.tipwindow or not self.text:
return

self.widget.mark_set(MARK_RIGHT, parenright)
self.anchor_widget.mark_set(MARK_RIGHT, parenright)
self.parenline, self.parencol = map(
int, self.widget.index(parenleft).split("."))
int, self.anchor_widget.index(parenleft).split("."))

self.tipwindow = tw = Toplevel(self.widget)
self.position_window()
# remove border on calltip 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.label = Label(tw, text=self.text, justify=LEFT,
super(CalltipWindow, self).showtip()

self._bind_events()

def showcontents(self):
"""Create the call-tip widget."""
self.label = Label(self.tipwindow, text=self.text, justify=LEFT,
background="#ffffe0", relief=SOLID, borderwidth=1,
font = self.widget['font'])
font=self.anchor_widget['font'])
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):
"""Handle CHECK_HIDE_EVENT: call hidetip or reschedule."""
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,
# so do nothing in this case.
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 \
(curline == self.parenline and curcol <= self.parencol) or \
self.widget.compare("insert", ">", MARK_RIGHT):
self.anchor_widget.compare("insert", ">", MARK_RIGHT):
self.hidetip()
return "break"
else:
self.position_window()
if self.checkhide_after_id is not None:
self.widget.after_cancel(self.checkhide_after_id)
self.checkhide_after_id = \
self.widget.after(CHECKHIDE_TIME, self.checkhide_event)
return None

# Not hiding the call-tip.

self.position_window()
# Re-schedule this function to be called again in a short while.
if self.checkhide_after_id is not None:
self.anchor_widget.after_cancel(self.checkhide_after_id)
self.checkhide_after_id = \
self.anchor_widget.after(CHECKHIDE_TIME, self.checkhide_event)
return None

def hide_event(self, event):
"""Handle HIDE_EVENT by calling hidetip."""
if not self.tipwindow:
# See the explanation in checkhide_event.
return None
self.hidetip()
return "break"

def hidetip(self):
"""Hide the call-tip."""
if not self.tipwindow:
return

for seq in CHECKHIDE_SEQUENCES:
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()
try:
self.label.destroy()
except TclError:
pass
self.label = None
self.tipwindow.destroy()
self.tipwindow = None

self.widget.mark_unset(MARK_RIGHT)
self.parenline = self.parencol = self.lastline = None
try:
self.anchor_widget.mark_unset(MARK_RIGHT)
except TclError:
pass

def is_active(self):
return bool(self.tipwindow)
try:
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 #
from tkinter import Toplevel, Text, LEFT, BOTH

top = Toplevel(parent)
top.title("Test calltips")
top.title("Test call-tips")
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.pack(side=LEFT, fill=BOTH, expand=1)
text.insert("insert", "string.split")
top.update()
calltip = CalltipWindow(text)

calltip = CalltipWindow(text)
def calltip_show(event):
calltip.showtip("(s=Hello world)", "insert", "end")
calltip.showtip("(s='Hello world')", "insert", "end")
def calltip_hide(event):
calltip.hidetip()
text.event_add("<<calltip-show>>", "(")
text.event_add("<<calltip-hide>>", ")")
text.bind("<<calltip-show>>", calltip_show)
text.bind("<<calltip-hide>>", calltip_hide)

text.focus_set()

if __name__ == '__main__':
Expand Down
3 changes: 3 additions & 0 deletions Lib/idlelib/idle_test/htest.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,14 @@ def _wrapper(parent): # htest #
"are correctly displayed.\n [Close] to exit.",
}

# TODO implement ^\; adding '<Control-Key-\\>' to function does not work.
_calltip_window_spec = {
'file': 'calltip_w',
'kwds': {},
'msg': "Typing '(' should display a 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 = {
Expand Down
2 changes: 1 addition & 1 deletion Lib/idlelib/idle_test/test_calltip_w.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def tearDownClass(cls):
del cls.text, cls.root

def test_init(self):
self.assertEqual(self.calltip.widget, self.text)
self.assertEqual(self.calltip.anchor_widget, self.text)

if __name__ == '__main__':
unittest.main(verbosity=2)
Loading