diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index 58459017622067..09431b70df50bc 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -1954,6 +1954,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: @@ -4707,6 +4727,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 692b3cfdc067b8..4a8dcb76690060 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -178,6 +178,12 @@ tkinter validation command. (Contributed by Serhiy Storchaka in :gh:`151878`.) +* 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`.) + * Added new window-management methods :meth:`~tkinter.Misc.winfo_isdark` (dark mode detection), :meth:`~tkinter.Wm.wm_iconbadge` (application icon badge) and :meth:`~tkinter.Wm.wm_stackorder` (toplevel stacking order). diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index 77bf84304e78ef..80dc163fc18de4 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 d0305562a0cb05..353a99a35544c1 100644 --- a/Lib/test/test_tkinter/test_widgets.py +++ b/Lib/test/test_tkinter/test_widgets.py @@ -2566,6 +2566,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 974e386be1bb49..eb4e23c5556643 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. @@ -3725,6 +3751,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.