Skip to content
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

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

Merged
merged 1 commit into from
Aug 5, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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