Skip to content

Commit e55966b

Browse files
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWevzas4XVpjzedzR9gKVo
1 parent f28ef85 commit e55966b

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
@@ -1932,6 +1932,26 @@ Base and mixin classes
19321932
A true *boolean* value enables strict Motif compliance (for example, no
19331933
color change when the mouse passes over a slider).
19341934
Return the resulting setting.
1935+
1936+
.. method:: tk_scaling(number=None, *, displayof=0)
1937+
1938+
Query or set the scaling factor used by Tk to convert between physical
1939+
units (such as points, inches or millimeters) and pixels, expressed as
1940+
the number of pixels per point (where a point is 1/72 inch).
1941+
With no argument, return the current factor; otherwise set it to the
1942+
floating-point *number*.
1943+
1944+
.. versionadded:: next
1945+
1946+
.. method:: tk_inactive(reset=False, *, displayof=0)
1947+
1948+
Return the number of milliseconds since the last time the user interacted
1949+
with the system, or ``-1`` if the windowing system does not support this.
1950+
If *reset* is true, reset the inactivity timer to zero instead and return
1951+
``None``.
1952+
1953+
.. versionadded:: next
1954+
19351955
.. method:: busy(**kw)
19361956
:no-typesetting:
19371957

@@ -4572,6 +4592,15 @@ Widget classes
45724592
If the *postcommand* option has been specified, it is evaluated before
45734593
the menu is posted.
45744594

4595+
.. method:: postcascade(index)
4596+
4597+
Post the submenu associated with the cascade entry given by *index*,
4598+
unposting any previously posted submenu.
4599+
This has no effect if *index* does not name a cascade entry or if the
4600+
menu itself is not posted.
4601+
4602+
.. versionadded:: next
4603+
45754604
.. method:: tk_popup(x, y, entry='')
45764605

45774606
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
@@ -157,6 +157,12 @@ tkinter
157157
synchronization of the displayed view with the underlying text.
158158
(Contributed by Serhiy Storchaka in :gh:`151675`.)
159159

160+
* Added the :meth:`tkinter.Menu.postcascade` method, and the
161+
:meth:`~tkinter.Misc.tk_scaling` and :meth:`~tkinter.Misc.tk_inactive`
162+
methods which respectively query or set the display scaling factor and
163+
report the user idle time.
164+
(Contributed by Serhiy Storchaka in :gh:`151881`.)
165+
160166
xml
161167
---
162168

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
@@ -2547,6 +2547,35 @@ def test_post_unpost(self):
25472547
m.update()
25482548
self.assertFalse(m.winfo_ismapped())
25492549

2550+
def test_postcascade(self):
2551+
m = self.create(tearoff=False)
2552+
submenu = tkinter.Menu(m, tearoff=False)
2553+
submenu.add_command(label='Item')
2554+
m.add_cascade(label='Cascade', menu=submenu)
2555+
m.add_command(label='Plain')
2556+
# No effect (but no error) when the menu is not posted, when the index
2557+
# is not a cascade entry, or when given a label.
2558+
m.postcascade(0)
2559+
m.postcascade(1)
2560+
m.postcascade('Cascade')
2561+
2562+
with self.subTest('posted menu'):
2563+
if m._windowingsystem != 'x11':
2564+
# Posting a menu is modal on Windows and uses a native,
2565+
# unmapped menu on Aqua, so it cannot be tested synchronously
2566+
# there.
2567+
self.skipTest('menu posting is not testable on this platform')
2568+
m.post(0, 0)
2569+
m.update()
2570+
m.postcascade('Cascade')
2571+
m.update()
2572+
self.assertTrue(submenu.winfo_ismapped())
2573+
# A non-cascade index unposts the currently posted submenu.
2574+
m.postcascade(1)
2575+
m.update()
2576+
self.assertFalse(submenu.winfo_ismapped())
2577+
m.unpost()
2578+
25502579
def check_entry_option(self, m, index, option, value, expected=None):
25512580
if expected is None:
25522581
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
@@ -3673,6 +3699,14 @@ def post(self, x, y):
36733699
"""Display a menu at position X,Y."""
36743700
self.tk.call(self._w, 'post', x, y)
36753701

3702+
def postcascade(self, index):
3703+
"""Post the submenu of the cascade entry at INDEX, unposting any
3704+
previously posted submenu.
3705+
3706+
Has no effect if INDEX does not name a cascade entry or if this menu
3707+
is not posted."""
3708+
self.tk.call(self._w, 'postcascade', index)
3709+
36763710
def type(self, index):
36773711
"""Return the type of the menu item at INDEX."""
36783712
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)