https://github.com/python/cpython/commit/6185dfb450a3a76d2451cf26066acaf0fded8bf2
commit: 6185dfb450a3a76d2451cf26066acaf0fded8bf2
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-06-22T13:53:02Z
summary:

gh-151881: Add tkinter Menu.postcascade, Misc.tk_scaling and tk_inactive 
(GH-151882)

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 <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-06-22-01-39-38.gh-issue-151881.ShACSZ.rst
M Doc/library/tkinter.rst
M Doc/whatsnew/3.16.rst
M Lib/test/test_tkinter/test_misc.py
M Lib/test/test_tkinter/test_widgets.py
M Lib/tkinter/__init__.py

diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst
index a9659567eab5a7..95b4a088ef9114 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:
 
@@ -4727,6 +4747,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 ec7329a3d85260..1dc6a62cc16d2d 100644
--- a/Doc/whatsnew/3.16.rst
+++ b/Doc/whatsnew/3.16.rst
@@ -183,6 +183,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 c2117981208dd5..e388f0e8ed8f93 100644
--- a/Lib/test/test_tkinter/test_widgets.py
+++ b/Lib/test/test_tkinter/test_widgets.py
@@ -2594,6 +2594,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 bf6a75875cfcb7..7fcc7d764da317 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.
 
@@ -3741,6 +3767,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.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to