Skip to content

Commit 6185dfb

Browse files
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 <noreply@anthropic.com>
1 parent b9bceb1 commit 6185dfb

6 files changed

Lines changed: 120 additions & 0 deletions

File tree

Doc/library/tkinter.rst

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1954,6 +1954,26 @@ Base and mixin classes
19541954
A true *boolean* value enables strict Motif compliance (for example, no
19551955
color change when the mouse passes over a slider).
19561956
Return the resulting setting.
1957+
1958+
.. method:: tk_scaling(number=None, *, displayof=0)
1959+
1960+
Query or set the scaling factor used by Tk to convert between physical
1961+
units (such as points, inches or millimeters) and pixels, expressed as
1962+
the number of pixels per point (where a point is 1/72 inch).
1963+
With no argument, return the current factor; otherwise set it to the
1964+
floating-point *number*.
1965+
1966+
.. versionadded:: next
1967+
1968+
.. method:: tk_inactive(reset=False, *, displayof=0)
1969+
1970+
Return the number of milliseconds since the last time the user interacted
1971+
with the system, or ``-1`` if the windowing system does not support this.
1972+
If *reset* is true, reset the inactivity timer to zero instead and return
1973+
``None``.
1974+
1975+
.. versionadded:: next
1976+
19571977
.. method:: busy(**kw)
19581978
:no-typesetting:
19591979

@@ -4727,6 +4747,15 @@ Widget classes
47274747
If the *postcommand* option has been specified, it is evaluated before
47284748
the menu is posted.
47294749

4750+
.. method:: postcascade(index)
4751+
4752+
Post the submenu associated with the cascade entry given by *index*,
4753+
unposting any previously posted submenu.
4754+
This has no effect if *index* does not name a cascade entry or if the
4755+
menu itself is not posted.
4756+
4757+
.. versionadded:: next
4758+
47304759
.. method:: tk_popup(x, y, entry='')
47314760

47324761
Post the menu as a popup at the root-window coordinates *x* and *y*.

Doc/whatsnew/3.16.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,12 @@ tkinter
183183
validation command.
184184
(Contributed by Serhiy Storchaka in :gh:`151878`.)
185185

186+
* Added the :meth:`tkinter.Menu.postcascade` method, and the
187+
:meth:`~tkinter.Misc.tk_scaling` and :meth:`~tkinter.Misc.tk_inactive`
188+
methods which respectively query or set the display scaling factor and
189+
report the user idle time.
190+
(Contributed by Serhiy Storchaka in :gh:`151881`.)
191+
186192
* Added new window-management methods :meth:`~tkinter.Misc.winfo_isdark`
187193
(dark mode detection), :meth:`~tkinter.Wm.wm_iconbadge` (application icon
188194
badge) and :meth:`~tkinter.Wm.wm_stackorder` (toplevel stacking order).

Lib/test/test_tkinter/test_misc.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,24 @@ def test_tk_bisque(self):
463463
self.assertEqual(root['background'], '#ffe4c4')
464464
self.assertRaises(TypeError, root.tk_bisque, 'x')
465465

466+
def test_tk_scaling(self):
467+
old = self.root.tk_scaling()
468+
self.assertIsInstance(old, float)
469+
self.assertGreater(old, 0)
470+
self.addCleanup(self.root.tk_scaling, old)
471+
# Setting the factor is reflected by a subsequent query. Tk may round
472+
# it slightly when converting to and from its internal representation.
473+
self.root.tk_scaling(2.0)
474+
self.assertAlmostEqual(self.root.tk_scaling(), 2.0, delta=0.1)
475+
476+
def test_tk_inactive(self):
477+
ms = self.root.tk_inactive()
478+
self.assertIsInstance(ms, int)
479+
# A count of milliseconds, or -1 if the windowing system lacks support.
480+
self.assertGreaterEqual(ms, -1)
481+
# Resetting the timer returns None and does not raise.
482+
self.assertIsNone(self.root.tk_inactive(reset=True))
483+
466484
def test_wait_variable(self):
467485
var = tkinter.StringVar(self.root)
468486
self.assertEqual(self.root.waitvar, self.root.wait_variable)

Lib/test/test_tkinter/test_widgets.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2594,6 +2594,35 @@ def test_post_unpost(self):
25942594
m.update()
25952595
self.assertFalse(m.winfo_ismapped())
25962596

2597+
def test_postcascade(self):
2598+
m = self.create(tearoff=False)
2599+
submenu = tkinter.Menu(m, tearoff=False)
2600+
submenu.add_command(label='Item')
2601+
m.add_cascade(label='Cascade', menu=submenu)
2602+
m.add_command(label='Plain')
2603+
# No effect (but no error) when the menu is not posted, when the index
2604+
# is not a cascade entry, or when given a label.
2605+
m.postcascade(0)
2606+
m.postcascade(1)
2607+
m.postcascade('Cascade')
2608+
2609+
with self.subTest('posted menu'):
2610+
if m._windowingsystem != 'x11':
2611+
# Posting a menu is modal on Windows and uses a native,
2612+
# unmapped menu on Aqua, so it cannot be tested synchronously
2613+
# there.
2614+
self.skipTest('menu posting is not testable on this platform')
2615+
m.post(0, 0)
2616+
m.update()
2617+
m.postcascade('Cascade')
2618+
m.update()
2619+
self.assertTrue(submenu.winfo_ismapped())
2620+
# A non-cascade index unposts the currently posted submenu.
2621+
m.postcascade(1)
2622+
m.update()
2623+
self.assertFalse(submenu.winfo_ismapped())
2624+
m.unpost()
2625+
25972626
def check_entry_option(self, m, index, option, value, expected=None):
25982627
if expected is None:
25992628
expected = value

Lib/tkinter/__init__.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,32 @@ def tk_setPalette(self, *args, **kw):
745745
self.tk.call(('tk_setPalette',)
746746
+ _flatten(args) + _flatten(list(kw.items())))
747747

748+
def tk_scaling(self, number=None, *, displayof=0):
749+
"""Query or set the scaling factor used by Tk to convert between
750+
physical units and pixels.
751+
752+
The scaling factor is the number of pixels per point on the display,
753+
where a point is 1/72 inch. With no argument, return the current
754+
factor; otherwise set it to the floating-point NUMBER."""
755+
args = ('tk', 'scaling') + self._displayof(displayof)
756+
if number is not None:
757+
self.tk.call(args + (number,))
758+
else:
759+
return self.tk.getdouble(self.tk.call(args))
760+
761+
def tk_inactive(self, reset=False, *, displayof=0):
762+
"""Return the number of milliseconds since the last time the user
763+
interacted with the system, or -1 if the windowing system does not
764+
support this.
765+
766+
If RESET is true, reset the inactivity timer to zero instead and
767+
return None."""
768+
args = ('tk', 'inactive') + self._displayof(displayof)
769+
if reset:
770+
self.tk.call(args + ('reset',))
771+
else:
772+
return self.tk.getint(self.tk.call(args))
773+
748774
def wait_variable(self, name='PY_VAR'):
749775
"""Wait until the variable is modified.
750776
@@ -3741,6 +3767,14 @@ def post(self, x, y):
37413767
"""Display a menu at position X,Y."""
37423768
self.tk.call(self._w, 'post', x, y)
37433769

3770+
def postcascade(self, index):
3771+
"""Post the submenu of the cascade entry at INDEX, unposting any
3772+
previously posted submenu.
3773+
3774+
Has no effect if INDEX does not name a cascade entry or if this menu
3775+
is not posted."""
3776+
self.tk.call(self._w, 'postcascade', index)
3777+
37443778
def type(self, index):
37453779
"""Return the type of the menu item at INDEX."""
37463780
return self.tk.call(self._w, 'type', index)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Add the :meth:`tkinter.Menu.postcascade` method and the
2+
:meth:`!tkinter.Misc.tk_scaling` and :meth:`!tkinter.Misc.tk_inactive`
3+
methods, wrapping the ``postcascade``, ``tk scaling`` and ``tk inactive``
4+
Tk commands.

0 commit comments

Comments
 (0)