]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-151881: Add tkinter Menu.postcascade, Misc.tk_scaling and tk_inactive (GH-151882)
authorSerhiy Storchaka <storchaka@gmail.com>
Mon, 22 Jun 2026 13:53:02 +0000 (16:53 +0300)
committerGitHub <noreply@github.com>
Mon, 22 Jun 2026 13:53:02 +0000 (13:53 +0000)
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 <noreply@anthropic.com>
Doc/library/tkinter.rst
Doc/whatsnew/3.16.rst
Lib/test/test_tkinter/test_misc.py
Lib/test/test_tkinter/test_widgets.py
Lib/tkinter/__init__.py
Misc/NEWS.d/next/Library/2026-06-22-01-39-38.gh-issue-151881.ShACSZ.rst [new file with mode: 0644]

index a9659567eab5a799968de26deee048fb6a80f528..95b4a088ef9114e067a831fca10ab7002640d3fa 100644 (file)
@@ -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*.
index ec7329a3d852609f8efe0e28d34ad5db1d4e3f5e..1dc6a62cc16d2de06da96c5c2e99a7e8a794e657 100644 (file)
@@ -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).
index 77bf84304e78ef096687f95915ee5f4c6b4e306b..80dc163fc18de412158cb014d7e1738075d7706b 100644 (file)
@@ -463,6 +463,24 @@ class MiscTest(AbstractTkTest, unittest.TestCase):
         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)
index c2117981208dd5c062bcf58dead1c5f446b115d9..e388f0e8ed8f93d386efda879265f76ed3df83f7 100644 (file)
@@ -2594,6 +2594,35 @@ class MenuTest(AbstractWidgetTest, unittest.TestCase):
         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
index bf6a75875cfcb7ee8cebf90f15e6903e2f9a9c41..7fcc7d764da317c4605a99cda6324c7d60e2a3d4 100644 (file)
@@ -745,6 +745,32 @@ class Misc:
         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 @@ class Menu(Widget):
         """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 (file)
index 0000000..9ae9394
--- /dev/null
@@ -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.