From 79db9d9a0e8f51ad4ea5caae31d7ae4b29985271 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 19 Aug 2023 14:48:02 +0300 Subject: gh-72684: Tkinter: provide interface for "tk busy" subcommands (GH-107684) Add tkinter.Misc methods: tk_busy_hold(), tk_busy_configure(), tk_busy_cget(), tk_busy_forget(), tk_busy_current(), and tk_busy_status(). --- Doc/whatsnew/3.13.rst | 9 +++ Lib/test/test_tkinter/test_misc.py | 56 ++++++++++++++- Lib/tkinter/__init__.py | 79 ++++++++++++++++++++++ .../2023-08-06-10-52-12.gh-issue-72684.Ls2mSf.rst | 3 + 4 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2023-08-06-10-52-12.gh-issue-72684.Ls2mSf.rst diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 85aa81b..47b868b 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -144,6 +144,15 @@ pathlib :meth:`~pathlib.Path.is_dir`. (Contributed by Barney Gale in :gh:`77609` and :gh:`105793`.) +tkinter +------- + +* Add :mod:`tkinter` widget methods: + :meth:`!tk_busy_hold`, :meth:`!tk_busy_configure`, + :meth:`!tk_busy_cget`, :meth:`!tk_busy_forget`, + :meth:`!tk_busy_current`, and :meth:`!tk_busy_status`. + (Contributed by Miguel, klappnase and Serhiy Storchaka in :gh:`72684`.) + traceback --------- diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index d1aca58..0ae2761 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -1,9 +1,10 @@ import functools import unittest import tkinter +from tkinter import TclError import enum from test import support -from test.test_tkinter.support import AbstractTkTest, AbstractDefaultRootTest +from test.test_tkinter.support import AbstractTkTest, AbstractDefaultRootTest, requires_tk support.requires('gui') @@ -36,6 +37,59 @@ class MiscTest(AbstractTkTest, unittest.TestCase): for name in str(b).split('.'): self.assertFalse(name.isidentifier(), msg=repr(name)) + @requires_tk(8, 6, 6) + def test_tk_busy(self): + root = self.root + f = tkinter.Frame(root, name='myframe') + f2 = tkinter.Frame(root) + f.pack() + f2.pack() + b = tkinter.Button(f) + b.pack() + f.tk_busy_hold() + with self.assertRaisesRegex(TclError, 'unknown option "-spam"'): + f.tk_busy_configure(spam='eggs') + with self.assertRaisesRegex(TclError, 'unknown option "-spam"'): + f.tk_busy_cget('spam') + with self.assertRaisesRegex(TclError, 'unknown option "-spam"'): + f.tk_busy_configure('spam') + self.assertIsInstance(f.tk_busy_configure(), dict) + + self.assertTrue(f.tk_busy_status()) + self.assertFalse(root.tk_busy_status()) + self.assertFalse(f2.tk_busy_status()) + self.assertFalse(b.tk_busy_status()) + self.assertIn(f, f.tk_busy_current()) + self.assertIn(f, f.tk_busy_current('*.m?f*me')) + self.assertNotIn(f, f.tk_busy_current('*spam')) + + f.tk_busy_forget() + self.assertFalse(f.tk_busy_status()) + self.assertFalse(f.tk_busy_current()) + with self.assertRaisesRegex(TclError, "can't find busy window"): + f.tk_busy_configure() + with self.assertRaisesRegex(TclError, "can't find busy window"): + f.tk_busy_forget() + + @requires_tk(8, 6, 6) + def test_tk_busy_with_cursor(self): + root = self.root + if root._windowingsystem == 'aqua': + self.skipTest('the cursor option is not supported on OSX/Aqua') + f = tkinter.Frame(root, name='myframe') + f.pack() + f.tk_busy_hold(cursor='gumby') + + self.assertEqual(f.tk_busy_cget('cursor'), 'gumby') + f.tk_busy_configure(cursor='heart') + self.assertEqual(f.tk_busy_cget('cursor'), 'heart') + self.assertEqual(f.tk_busy_configure()['cursor'][4], 'heart') + self.assertEqual(f.tk_busy_configure('cursor')[4], 'heart') + + f.tk_busy_forget() + with self.assertRaisesRegex(TclError, "can't find busy window"): + f.tk_busy_cget('cursor') + def test_tk_setPalette(self): root = self.root root.tk_setPalette('black') diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index c59f8d1..440e7f1 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -901,6 +901,85 @@ class Misc: """Ring a display's bell.""" self.tk.call(('bell',) + self._displayof(displayof)) + def tk_busy_cget(self, option): + """Return the value of busy configuration option. + + The widget must have been previously made busy by + tk_busy_hold(). Option may have any of the values accepted by + tk_busy_hold(). + """ + return self.tk.call('tk', 'busy', 'cget', self._w, '-'+option) + busy_cget = tk_busy_cget + + def tk_busy_configure(self, cnf=None, **kw): + """Query or modify the busy configuration options. + + The widget must have been previously made busy by + tk_busy_hold(). Options may have any of the values accepted by + tk_busy_hold(). + + Please note that the option database is referenced by the widget + name or class. For example, if a Frame widget with name "frame" + is to be made busy, the busy cursor can be specified for it by + either call: + + w.option_add('*frame.busyCursor', 'gumby') + w.option_add('*Frame.BusyCursor', 'gumby') + """ + if kw: + cnf = _cnfmerge((cnf, kw)) + elif cnf: + cnf = _cnfmerge(cnf) + if cnf is None: + return self._getconfigure( + 'tk', 'busy', 'configure', self._w) + if isinstance(cnf, str): + return self._getconfigure1( + 'tk', 'busy', 'configure', self._w, '-'+cnf) + self.tk.call('tk', 'busy', 'configure', self._w, *self._options(cnf)) + busy_config = busy_configure = tk_busy_config = tk_busy_configure + + def tk_busy_current(self, pattern=None): + """Return a list of widgets that are currently busy. + + If a pattern is given, only busy widgets whose path names match + a pattern are returned. + """ + return [self._nametowidget(x) for x in + self.tk.splitlist(self.tk.call( + 'tk', 'busy', 'current', pattern))] + busy_current = tk_busy_current + + def tk_busy_forget(self): + """Make this widget no longer busy. + + User events will again be received by the widget. + """ + self.tk.call('tk', 'busy', 'forget', self._w) + busy_forget = tk_busy_forget + + def tk_busy_hold(self, **kw): + """Make this widget appear busy. + + The specified widget and its descendants will be blocked from + user interactions. Normally update() should be called + immediately afterward to insure that the hold operation is in + effect before the application starts its processing. + + The only supported configuration option is: + + cursor: the cursor to be displayed when the widget is made + busy. + """ + self.tk.call('tk', 'busy', 'hold', self._w, *self._options(kw)) + busy = busy_hold = tk_busy = tk_busy_hold + + def tk_busy_status(self): + """Return True if the widget is busy, False otherwise.""" + return self.tk.getboolean(self.tk.call( + 'tk', 'busy', 'status', self._w)) + busy_status = tk_busy_status + # Clipboard handling: def clipboard_get(self, **kw): """Retrieve data from the clipboard on window's display. diff --git a/Misc/NEWS.d/next/Library/2023-08-06-10-52-12.gh-issue-72684.Ls2mSf.rst b/Misc/NEWS.d/next/Library/2023-08-06-10-52-12.gh-issue-72684.Ls2mSf.rst new file mode 100644 index 0000000..1ac0e6d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-08-06-10-52-12.gh-issue-72684.Ls2mSf.rst @@ -0,0 +1,3 @@ +Add :mod:`tkinter` widget methods: :meth:`!tk_busy_hold`, +:meth:`!tk_busy_configure`, :meth:`!tk_busy_cget`, :meth:`!tk_busy_forget`, +:meth:`!tk_busy_current`, and :meth:`!tk_busy_status`. -- cgit v0.12