From e55966b5c8bea56143c040130d57f9971e270500 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 22 Jun 2026 01:40:28 +0300 Subject: [PATCH] gh-151881: Add tkinter Menu.postcascade, Misc.tk_scaling and tk_inactive Wrap three long-standing Tk commands that had no tkinter wrapper: * Menu.postcascade() posts the submenu of a cascade entry (Tk 8.5), complementing the existing post() and unpost() methods. * Misc.tk_scaling() queries or sets the scaling factor in pixels per point used to convert between physical units and pixels (Tk 8.4). * Misc.tk_inactive() returns the user idle time in milliseconds, and can reset that timer (Tk 8.5). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XWevzas4XVpjzedzR9gKVo --- Doc/library/tkinter.rst | 29 ++++++++++++++++ Doc/whatsnew/3.16.rst | 6 ++++ Lib/test/test_tkinter/test_misc.py | 18 ++++++++++ Lib/test/test_tkinter/test_widgets.py | 29 ++++++++++++++++ Lib/tkinter/__init__.py | 34 +++++++++++++++++++ ...-06-22-01-39-38.gh-issue-151881.ShACSZ.rst | 4 +++ 6 files changed, 120 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-06-22-01-39-38.gh-issue-151881.ShACSZ.rst diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index 222a4d0128bd8d..0c27f973cebeaf 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -1932,6 +1932,26 @@ Base and mixin classes A true *boolean* value enables strict Motif compliance (for example, no color change when the mouse passes over a slider). Return the resulting setting. + + .. method:: tk_scaling(number=None, *, displayof=0) + + Query or set the scaling factor used by Tk to convert between physical + units (such as points, inches or millimeters) and pixels, expressed as + the number of pixels per point (where a point is 1/72 inch). + With no argument, return the current factor; otherwise set it to the + floating-point *number*. + + .. versionadded:: next + + .. method:: tk_inactive(reset=False, *, displayof=0) + + Return the number of milliseconds since the last time the user interacted + with the system, or ``-1`` if the windowing system does not support this. + If *reset* is true, reset the inactivity timer to zero instead and return + ``None``. + + .. versionadded:: next + .. method:: busy(**kw) :no-typesetting: @@ -4572,6 +4592,15 @@ Widget classes If the *postcommand* option has been specified, it is evaluated before the menu is posted. + .. method:: postcascade(index) + + Post the submenu associated with the cascade entry given by *index*, + unposting any previously posted submenu. + This has no effect if *index* does not name a cascade entry or if the + menu itself is not posted. + + .. versionadded:: next + .. method:: tk_popup(x, y, entry='') Post the menu as a popup at the root-window coordinates *x* and *y*. diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index ec8e367d938ddb..97514203bac999 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -157,6 +157,12 @@ tkinter synchronization of the displayed view with the underlying text. (Contributed by Serhiy Storchaka in :gh:`151675`.) +* Added the :meth:`tkinter.Menu.postcascade` method, and the + :meth:`~tkinter.Misc.tk_scaling` and :meth:`~tkinter.Misc.tk_inactive` + methods which respectively query or set the display scaling factor and + report the user idle time. + (Contributed by Serhiy Storchaka in :gh:`151881`.) + xml --- diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index b4cb5aaae1b1e5..06b5b2e6e8f627 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -463,6 +463,24 @@ def test_tk_bisque(self): self.assertEqual(root['background'], '#ffe4c4') self.assertRaises(TypeError, root.tk_bisque, 'x') + def test_tk_scaling(self): + old = self.root.tk_scaling() + self.assertIsInstance(old, float) + self.assertGreater(old, 0) + self.addCleanup(self.root.tk_scaling, old) + # Setting the factor is reflected by a subsequent query. Tk may round + # it slightly when converting to and from its internal representation. + self.root.tk_scaling(2.0) + self.assertAlmostEqual(self.root.tk_scaling(), 2.0, delta=0.1) + + def test_tk_inactive(self): + ms = self.root.tk_inactive() + self.assertIsInstance(ms, int) + # A count of milliseconds, or -1 if the windowing system lacks support. + self.assertGreaterEqual(ms, -1) + # Resetting the timer returns None and does not raise. + self.assertIsNone(self.root.tk_inactive(reset=True)) + def test_wait_variable(self): var = tkinter.StringVar(self.root) self.assertEqual(self.root.waitvar, self.root.wait_variable) diff --git a/Lib/test/test_tkinter/test_widgets.py b/Lib/test/test_tkinter/test_widgets.py index 4b51d219d87e5b..c52eb937063b6d 100644 --- a/Lib/test/test_tkinter/test_widgets.py +++ b/Lib/test/test_tkinter/test_widgets.py @@ -2547,6 +2547,35 @@ def test_post_unpost(self): m.update() self.assertFalse(m.winfo_ismapped()) + def test_postcascade(self): + m = self.create(tearoff=False) + submenu = tkinter.Menu(m, tearoff=False) + submenu.add_command(label='Item') + m.add_cascade(label='Cascade', menu=submenu) + m.add_command(label='Plain') + # No effect (but no error) when the menu is not posted, when the index + # is not a cascade entry, or when given a label. + m.postcascade(0) + m.postcascade(1) + m.postcascade('Cascade') + + with self.subTest('posted menu'): + if m._windowingsystem != 'x11': + # Posting a menu is modal on Windows and uses a native, + # unmapped menu on Aqua, so it cannot be tested synchronously + # there. + self.skipTest('menu posting is not testable on this platform') + m.post(0, 0) + m.update() + m.postcascade('Cascade') + m.update() + self.assertTrue(submenu.winfo_ismapped()) + # A non-cascade index unposts the currently posted submenu. + m.postcascade(1) + m.update() + self.assertFalse(submenu.winfo_ismapped()) + m.unpost() + def check_entry_option(self, m, index, option, value, expected=None): if expected is None: expected = value diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index 8bdf7cc1e2d96b..bbd4da0dc27cc5 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -745,6 +745,32 @@ def tk_setPalette(self, *args, **kw): self.tk.call(('tk_setPalette',) + _flatten(args) + _flatten(list(kw.items()))) + def tk_scaling(self, number=None, *, displayof=0): + """Query or set the scaling factor used by Tk to convert between + physical units and pixels. + + The scaling factor is the number of pixels per point on the display, + where a point is 1/72 inch. With no argument, return the current + factor; otherwise set it to the floating-point NUMBER.""" + args = ('tk', 'scaling') + self._displayof(displayof) + if number is not None: + self.tk.call(args + (number,)) + else: + return self.tk.getdouble(self.tk.call(args)) + + def tk_inactive(self, reset=False, *, displayof=0): + """Return the number of milliseconds since the last time the user + interacted with the system, or -1 if the windowing system does not + support this. + + If RESET is true, reset the inactivity timer to zero instead and + return None.""" + args = ('tk', 'inactive') + self._displayof(displayof) + if reset: + self.tk.call(args + ('reset',)) + else: + return self.tk.getint(self.tk.call(args)) + def wait_variable(self, name='PY_VAR'): """Wait until the variable is modified. @@ -3673,6 +3699,14 @@ def post(self, x, y): """Display a menu at position X,Y.""" self.tk.call(self._w, 'post', x, y) + def postcascade(self, index): + """Post the submenu of the cascade entry at INDEX, unposting any + previously posted submenu. + + Has no effect if INDEX does not name a cascade entry or if this menu + is not posted.""" + self.tk.call(self._w, 'postcascade', index) + def type(self, index): """Return the type of the menu item at INDEX.""" return self.tk.call(self._w, 'type', index) diff --git a/Misc/NEWS.d/next/Library/2026-06-22-01-39-38.gh-issue-151881.ShACSZ.rst b/Misc/NEWS.d/next/Library/2026-06-22-01-39-38.gh-issue-151881.ShACSZ.rst new file mode 100644 index 00000000000000..9ae939438e826f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-22-01-39-38.gh-issue-151881.ShACSZ.rst @@ -0,0 +1,4 @@ +Add the :meth:`tkinter.Menu.postcascade` method and the +:meth:`!tkinter.Misc.tk_scaling` and :meth:`!tkinter.Misc.tk_inactive` +methods, wrapping the ``postcascade``, ``tk scaling`` and ``tk inactive`` +Tk commands.