Skip to content

Commit

Permalink
bpo-33839: refactor IDLE's tooltips & calltips, add docstrings and te…
Browse files Browse the repository at this point in the history
…sts (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
(cherry picked from commit 87e59ac)

Co-authored-by: Tal Einat <taleinat+github@gmail.com>
  • Loading branch information
miss-islington and taleinat committed Aug 5, 2018
1 parent 2a2a3f5 commit e65ec49
Show file tree
Hide file tree
Showing 7 changed files with 416 additions and 144 deletions.
2 changes: 1 addition & 1 deletion Lib/idlelib/calltip.py
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
@@ -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
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
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)

0 comments on commit e65ec49

Please sign in to comment.