From 189c2686e7243fbc0ca9e81d9a76ffde8deb253e Mon Sep 17 00:00:00 2001 From: Shixian Li Date: Sun, 26 Jun 2022 22:23:57 +0800 Subject: [PATCH 1/3] Feat: Use ScrolledText widget to show tooltip --- Lib/idlelib/calltip.py | 12 ++---- Lib/idlelib/calltip_w.py | 12 +++++- Lib/idlelib/idle_test/test_calltip.py | 62 ++++++++------------------- 3 files changed, 30 insertions(+), 56 deletions(-) diff --git a/Lib/idlelib/calltip.py b/Lib/idlelib/calltip.py index 40bc5a0ad798fe..db49ac0bea82ee 100644 --- a/Lib/idlelib/calltip.py +++ b/Lib/idlelib/calltip.py @@ -182,19 +182,13 @@ def get_argspec(ob): # If fob has no argument, use default callable argspec. argspec = _default_callable_argspec - lines = (textwrap.wrap(argspec, _MAX_COLS, subsequent_indent=_INDENT) - if len(argspec) > _MAX_COLS else [argspec] if argspec else []) + lines = [argspec] if argspec else [] # Augment lines from docstring, if any, and join to get argspec. doc = inspect.getdoc(ob) if doc: - for line in doc.split('\n', _MAX_LINES)[:_MAX_LINES]: - line = line.strip() - if not line: - break - if len(line) > _MAX_COLS: - line = line[: _MAX_COLS - 3] + '...' - lines.append(line) + for line in doc.split('\n'): + lines.append(line.strip()) argspec = '\n'.join(lines) return argspec or _default_callable_argspec diff --git a/Lib/idlelib/calltip_w.py b/Lib/idlelib/calltip_w.py index 9386376058c791..5efe8983431038 100644 --- a/Lib/idlelib/calltip_w.py +++ b/Lib/idlelib/calltip_w.py @@ -4,11 +4,12 @@ Used by calltip.py. """ from tkinter import Label, LEFT, SOLID, TclError +from tkinter.scrolledtext import ScrolledText from idlelib.tooltip import TooltipBase HIDE_EVENT = "<>" -HIDE_SEQUENCES = ("", "") +HIDE_SEQUENCES = ("",) CHECKHIDE_EVENT = "<>" CHECKHIDE_SEQUENCES = ("", "") CHECKHIDE_TIME = 100 # milliseconds @@ -74,17 +75,22 @@ def showtip(self, text, parenleft, parenright): int, self.anchor_widget.index(parenleft).split(".")) super().showtip() + self.tipwindow.wm_attributes('-topmost', 1) self._bind_events() def showcontents(self): """Create the call-tip widget.""" - self.label = Label(self.tipwindow, text=self.text, justify=LEFT, + self.label = ScrolledText(self.tipwindow, wrap="word", background="#ffffd0", foreground="black", relief=SOLID, borderwidth=1, font=self.anchor_widget['font']) + self.label.insert('1.0', self.text) + self.label.config(state='disabled') self.label.pack() + self.tipwindow.geometry('%dx%d' % (400, 120)) + def checkhide_event(self, event=None): """Handle CHECK_HIDE_EVENT: call hidetip or reschedule.""" if not self.tipwindow: @@ -156,6 +162,8 @@ def _bind_events(self): self.hide_event) for seq in HIDE_SEQUENCES: self.anchor_widget.event_add(HIDE_EVENT, seq) + if self.tipwindow: + self.tipwindow.bind("", self.hide_event) def _unbind_events(self): """Unbind event handlers.""" diff --git a/Lib/idlelib/idle_test/test_calltip.py b/Lib/idlelib/idle_test/test_calltip.py index 28c196a42672fc..a6a2c49a31db9e 100644 --- a/Lib/idlelib/idle_test/test_calltip.py +++ b/Lib/idlelib/idle_test/test_calltip.py @@ -93,24 +93,20 @@ class SB: __call__ = None non-overlapping occurrences of the pattern in string by the replacement repl. repl can be either a string or a callable; if a string, backslash escapes in it are processed. If it is -a callable, it's passed the Match object and must return''') +a callable, it's passed the Match object and must return +a replacement string to be used.''') tiptest(p.sub, '''\ (repl, string, count=0) -Return the string obtained by replacing the leftmost \ -non-overlapping occurrences o...''') +Return the string obtained by replacing the leftmost non-overlapping \ +occurrences of pattern in string by the replacement repl.''') - def test_signature_wrap(self): + def test_signature(self): if textwrap.TextWrapper.__doc__ is not None: - self.assertEqual(get_spec(textwrap.TextWrapper), '''\ -(width=70, initial_indent='', subsequent_indent='', expand_tabs=True, - replace_whitespace=True, fix_sentence_endings=False, break_long_words=True, - drop_whitespace=True, break_on_hyphens=True, tabsize=8, *, max_lines=None, - placeholder=' [...]') -Object for wrapping/filling text. The public interface consists of -the wrap() and fill() methods; the other methods are just there for -subclasses to override in order to tweak the default behaviour. -If you want to completely replace the main wrapping algorithm, -you\'ll probably have to override _wrap_chunks().''') + self.assertEqual(get_spec(textwrap.TextWrapper).split('\n')[0], '''\ +(width=70, initial_indent='', subsequent_indent='', expand_tabs=True, \ +replace_whitespace=True, fix_sentence_endings=False, break_long_words=True, \ +drop_whitespace=True, break_on_hyphens=True, tabsize=8, *, max_lines=None, \ +placeholder=' [...]')''') def test_properly_formatted(self): @@ -127,16 +123,14 @@ def baz(s='a'*100, z='b'*100): indent = calltip._INDENT sfoo = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\ - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\ - "aaaaaaaaaa')" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')" sbar = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\ - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\ - "aaaaaaaaaa')\nHello Guido" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')"\ + "\nHello Guido" sbaz = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\ - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\ - "aaaaaaaaaa', z='bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"\ - "bbbbbbbbbbbbbbbbb\n" + indent + "bbbbbbbbbbbbbbbbbbbbbb"\ - "bbbbbbbbbbbbbbbbbbbbbb')" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',"\ + " z='bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"\ + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb')" for func,doc in [(foo, sfoo), (bar, sbar), (baz, sbaz)]: with self.subTest(func=func, doc=doc): @@ -145,29 +139,7 @@ def baz(s='a'*100, z='b'*100): def test_docline_truncation(self): def f(): pass f.__doc__ = 'a'*300 - self.assertEqual(get_spec(f), f"()\n{'a'*(calltip._MAX_COLS-3) + '...'}") - - @unittest.skipIf(MISSING_C_DOCSTRINGS, - "Signature information for builtins requires docstrings") - def test_multiline_docstring(self): - # Test fewer lines than max. - self.assertEqual(get_spec(range), - "range(stop) -> range object\n" - "range(start, stop[, step]) -> range object") - - # Test max lines - self.assertEqual(get_spec(bytes), '''\ -bytes(iterable_of_ints) -> bytes -bytes(string, encoding[, errors]) -> bytes -bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer -bytes(int) -> bytes object of size given by the parameter initialized with null bytes -bytes() -> empty bytes object''') - - def test_multiline_docstring_2(self): - # Test more than max lines - def f(): pass - f.__doc__ = 'a\n' * 15 - self.assertEqual(get_spec(f), '()' + '\na' * calltip._MAX_LINES) + self.assertEqual(get_spec(f), "()\n%s" % ('a'*300)) def test_functions(self): def t1(): 'doc' From ab496a5614b9ca90496ef4ea27dee8d7f5f1ef68 Mon Sep 17 00:00:00 2001 From: Shixian Li Date: Sun, 21 Dec 2025 04:25:42 +0800 Subject: [PATCH 2/3] Feat: Auto resize tooltip window to best size --- Lib/idlelib/calltip_w.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/Lib/idlelib/calltip_w.py b/Lib/idlelib/calltip_w.py index 5efe8983431038..9753c00fbe30ba 100644 --- a/Lib/idlelib/calltip_w.py +++ b/Lib/idlelib/calltip_w.py @@ -17,6 +17,13 @@ MARK_RIGHT = "calltipwindowregion_right" +def widget_size(widget): + widget.update() + width = widget.winfo_width() + height = widget.winfo_height() + return width, height + + class CalltipWindow(TooltipBase): """A call-tip widget for tkinter text widgets.""" @@ -75,21 +82,30 @@ def showtip(self, text, parenleft, parenright): int, self.anchor_widget.index(parenleft).split(".")) super().showtip() - self.tipwindow.wm_attributes('-topmost', 1) + self.tipwindow.wm_attributes("-topmost", 1) self._bind_events() def showcontents(self): """Create the call-tip widget.""" + self.label = Label(self.tipwindow, text=self.text, font=self.anchor_widget['font']) + self.label.pack() + label_w, label_h = widget_size(self.label) # get the old version of tooltip window size + self.label.forget() + self.label = ScrolledText(self.tipwindow, wrap="word", background="#ffffd0", foreground="black", relief=SOLID, borderwidth=1, - font=self.anchor_widget['font']) - self.label.insert('1.0', self.text) - self.label.config(state='disabled') + font=self.anchor_widget["font"]) + self.label.insert("1.0", self.text) + self.label.config(state="disabled") self.label.pack() + max_w, max_h = widget_size(self.label) + + if self.label.yview()[1] == 1: # already shown entire text + self.label.vbar.forget() - self.tipwindow.geometry('%dx%d' % (400, 120)) + self.tipwindow.geometry("%dx%d" % (min(label_w, max_w), min(label_h, max_h))) def checkhide_event(self, event=None): """Handle CHECK_HIDE_EVENT: call hidetip or reschedule.""" From 752344a25c47a7037133946495a3e487d9987499 Mon Sep 17 00:00:00 2001 From: Shixian Li Date: Sun, 21 Dec 2025 07:07:14 +0800 Subject: [PATCH 3/3] Adds news entries --- .../2025-12-21-07-02-44.gh-issue-94520.lqenne.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 Misc/NEWS.d/next/IDLE/2025-12-21-07-02-44.gh-issue-94520.lqenne.rst diff --git a/Misc/NEWS.d/next/IDLE/2025-12-21-07-02-44.gh-issue-94520.lqenne.rst b/Misc/NEWS.d/next/IDLE/2025-12-21-07-02-44.gh-issue-94520.lqenne.rst new file mode 100644 index 00000000000000..e5b72e0e47f330 --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2025-12-21-07-02-44.gh-issue-94520.lqenne.rst @@ -0,0 +1,11 @@ +Make CallTips selectable + +The text display widget in the "CalltipWindow" has been changed from +"tk.Label" to "ScrolledText", and now the text in the "Calltip" window can +be selected with mouse. The display size of the "CalltipWindow" is set to +the smaller value between the size when using the "tk.Label" widget and the +default size of "tk.Text". When the displayed text exceeds the display area +of the "ScrolledText" window, showing the vertical scrollbar; otherwise, +hiding the scrollbar. Since more text can be displayed, "argspec" is no +longer truncated, and the tests related to the max lines or text truncation +have been removed from the unit tests. Contributed by Shixian Li.