From 3432df2e28a95d0f6ef0dac401702f0020d188c0 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 27 Oct 2023 12:47:04 +0300 Subject: [PATCH 1/8] gh-68166: Fix support of "vsapi" in ttk.Style.element_create() * Fix and document support of "vsapi" element type in tkinter.ttk.Style.element_create(). * Add tests for element_create() and other ttk.Style methods. * Add examples for element_create() in the documentation. --- Doc/library/tkinter.ttk.rst | 71 ++++++++++- Lib/test/test_ttk/test_style.py | 120 ++++++++++++++++++ Lib/test/test_ttk_textonly.py | 14 +- Lib/tkinter/ttk.py | 51 ++++---- ...3-10-27-12-46-56.gh-issue-68166.0EbWW4.rst | 4 + 5 files changed, 228 insertions(+), 32 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-10-27-12-46-56.gh-issue-68166.0EbWW4.rst diff --git a/Doc/library/tkinter.ttk.rst b/Doc/library/tkinter.ttk.rst index dc31a1a4c1850a..4f47b70a26a391 100644 --- a/Doc/library/tkinter.ttk.rst +++ b/Doc/library/tkinter.ttk.rst @@ -1392,7 +1392,7 @@ option. If you don't know the class name of a widget, use the method Create a new element in the current theme, of the given *etype* which is expected to be either "image", "from" or "vsapi". The latter is only - available in Tk 8.6a for Windows XP and Vista and is not described here. + available in Tk 8.6a for Windows XP and Vista. If "image" is used, *args* should contain the default image name followed by statespec/value pairs (this is the imagespec), and *kw* may have the @@ -1418,6 +1418,16 @@ option. If you don't know the class name of a widget, use the method Specifies a minimum width for the element. If less than zero, the base image's width is used as a default. + Example:: + + img1 = tkinter.PhotoImage(master=root, file='button.png') + img1 = tkinter.PhotoImage(master=root, file='button-pressed.png') + img1 = tkinter.PhotoImage(master=root, file='button-active.png') + style = ttk.Style() + style.element_create('Button.button', 'image', + img1, ('pressed', img2), ('active', img3), + border=(2, 4), sticky='we') + If "from" is used as the value of *etype*, :meth:`element_create` will clone an existing element. *args* is expected to contain a themename, from which @@ -1425,6 +1435,65 @@ option. If you don't know the class name of a widget, use the method If this element to clone from is not specified, an empty element will be used. *kw* is discarded. + Example:: + + style = ttk.Style() + style.element_create('plain.background', 'from', 'default') + + If "vsapi" is used as the value of *etype*, :meth:`element_create` + will create a new element in the current theme whose visual appearance + is drawn using the Microsoft Visual Styles API which is responsible + for the themed styles on Windows XP and Vista. + *args* is expected to contain the Visual Styles class and part as + given in the Microsoft documentation followed by tuples of ttk states + and the corresponding Visual Styles API state value. + *kw* may have the following options: + + padding=padding + Specify the element's interior padding. + *padding* is a list of up to four integers specifying the left, + top, right and bottom padding quantities respectively. + If fewer than four elements are specified, bottom defaults to top, + right defaults to left, and top defaults to left. + In other words, a list of three numbers specify the left, vertical, + and right padding; a list of two numbers specify the horizontal + and the vertical padding; a single number specifies the same + padding all the way around the widget. + This option may not be mixed with any other options. + + margins=padding + Specifies the elements exterior padding. + *padding* is a list of up to four integers specifying the left, top, + right and bottom padding quantities respectively. + This option may not be mixed with any other options. + + width=width + Specifies the width for the element. + If this option is set then the Visual Styles API will not be queried + for the recommended size or the part. + If this option is set then *height* should also be set. + The *width* and *height* options cannot be mixed with the *padding* + or *margins* options. + + height=height + Specifies the height of the element. + See the comments for *width*. + + Example:: + + style = ttk.Style() + style.element_create('pin', 'vsapi', 'EXPLORERBAR', 3, + ('pressed', '!selected', 3), + ('active', '!selected', 2), + ('pressed', 'selected', 6), + ('active', 'selected', 5), + ('selected', 4), + ('', 1)) + style.layout('Explorer.Pin', + [('Explorer.Pin.pin', {'sticky': 'news'})]) + pin = ttk.Checkbutton(style='Explorer.Pin') + pin.pack(expand=True, fill='both') + .. method:: element_names() diff --git a/Lib/test/test_ttk/test_style.py b/Lib/test/test_ttk/test_style.py index f9c56ec2357451..3606493be7673a 100644 --- a/Lib/test/test_ttk/test_style.py +++ b/Lib/test/test_ttk/test_style.py @@ -2,6 +2,7 @@ import sys import tkinter from tkinter import ttk +from tkinter import TclError from test import support from test.support import requires from test.test_tkinter.support import AbstractTkTest, get_tk_patchlevel @@ -176,6 +177,125 @@ def test_map_custom_copy(self): for key, value in default.items(): self.assertEqual(style.map(newname, key), value) + def test_element_options(self): + style = self.style + element_names = style.element_names() + self.assertNotIsInstance(element_names, str) + for name in element_names: + self.assertIsInstance(name, str) + element_options = style.element_options(name) + self.assertNotIsInstance(element_options, str) + for optname in element_options: + self.assertIsInstance(optname, str) + + def test_element_create_errors(self): + style = self.style + with self.assertRaises(TypeError): + style.element_create('plain.newelem') + with self.assertRaisesRegex(TclError, 'No such element type spam'): + style.element_create('plain.newelem', 'spam') + + def test_element_create_from(self): + style = self.style + style.element_create('plain.background', 'from', 'default') + self.assertIn('plain.background', style.element_names()) + style.element_create('plain.arrow', 'from', 'default', 'rightarrow') + self.assertIn('plain.arrow', style.element_names()) + + def test_element_create_from_errors(self): + style = self.style + with self.assertRaises(IndexError): + style.element_create('plain.newelem', 'from') + with self.assertRaisesRegex(TclError, 'theme "spam" doesn\'t exist'): + style.element_create('plain.newelem', 'from', 'spam') + + def test_element_create_image(self): + style = self.style + image = tkinter.PhotoImage(master=self.root, width=10, height=10) + style.element_create('block', 'image', image) + self.assertIn('block', style.element_names()) + + imgfile = support.findfile('python.xbm', subdir='tkinterdata') + img1 = tkinter.BitmapImage(master=self.root, file=imgfile, + foreground='yellow', background='blue') + img2 = tkinter.BitmapImage(master=self.root, file=imgfile, + foreground='blue', background='yellow') + img3 = tkinter.BitmapImage(master=self.root, file=imgfile, + foreground='white', background='black') + style.element_create('Button.button', 'image', + img1, ('pressed', img2), ('active', img3), + border=(2, 4), sticky='we') + self.assertIn('Button.button', style.element_names()) + + def test_element_create_image_errors(self): + style = self.style + image = tkinter.PhotoImage(master=self.root, width=10, height=10) + with self.assertRaises(IndexError): + style.element_create('block2', 'image') + with self.assertRaises(TypeError): + style.element_create('block2', 'image', image, 1) + with self.assertRaises(ValueError): + style.element_create('block2', 'image', image, ()) + with self.assertRaisesRegex(TclError, 'Invalid state name'): + style.element_create('block2', 'image', image, ('spam', image)) + with self.assertRaisesRegex(TclError, 'Invalid state name'): + style.element_create('block2', 'image', image, (1, image)) + with self.assertRaises(TypeError): + style.element_create('block2', 'image', image, ('pressed', 1, image)) + with self.assertRaises(TypeError): + style.element_create('block2', 'image', image, (1, 'selected', image)) + with self.assertRaisesRegex(TclError, 'bad option'): + style.element_create('block2', 'image', image, spam=1) + + def test_element_create_vsapi_1(self): + style = self.style + if 'xpnative' not in style.theme_names(): + self.skipTest("requires 'xpnative' theme") + style.element_create('smallclose', 'vsapi', 'WINDOW', 19, + ('disabled', 4), + ('pressed', 3), + ('active', 2), + ('', 1)) + style.layout('CloseButton', + [('CloseButton.smallclose', {'sticky': 'news'})]) + b = ttk.Button(style='CloseButton') + b.pack(expand=True, fill='both') + self.assertEqual(b.winfo_reqwidth(), 13) + self.assertEqual(b.winfo_reqheight(), 13) + + def test_element_create_vsapi_2(self): + style = self.style + if 'xpnative' not in style.theme_names(): + self.skipTest("requires 'xpnative' theme") + style.element_create('pin', 'vsapi', 'EXPLORERBAR', 3, + ('pressed', '!selected', 3), + ('active', '!selected', 2), + ('pressed', 'selected', 6), + ('active', 'selected', 5), + ('selected', 4), + ('', 1)) + style.layout('Explorer.Pin', + [('Explorer.Pin.pin', {'sticky': 'news'})]) + pin = ttk.Checkbutton(style='Explorer.Pin') + pin.pack(expand=True, fill='both') + self.assertEqual(pin.winfo_reqwidth(), 16) + self.assertEqual(pin.winfo_reqheight(), 16) + + def test_element_create_vsapi_3(self): + style = self.style + if 'xpnative' not in style.theme_names(): + self.skipTest("requires 'xpnative' theme") + style.element_create('headerclose', 'vsapi', 'EXPLORERBAR', 2, + ('pressed', 3), + ('active', 2), + ('', 1)) + style.layout('Explorer.CloseButton', + [('Explorer.CloseButton.headerclose', {'sticky': 'news'})]) + b = ttk.Button(style='Explorer.CloseButton') + b.pack(expand=True, fill='both') + self.assertEqual(b.winfo_reqwidth(), 16) + self.assertEqual(b.winfo_reqheight(), 16) + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_ttk_textonly.py b/Lib/test/test_ttk_textonly.py index 96dc179a69ecac..1c4113fadd0624 100644 --- a/Lib/test/test_ttk_textonly.py +++ b/Lib/test/test_ttk_textonly.py @@ -179,7 +179,7 @@ def test_format_elemcreate(self): # don't format returned values as a tcl script # minimum acceptable for image type self.assertEqual(ttk._format_elemcreate('image', False, 'test'), - ("test ", ())) + ("test", ())) # specifying a state spec self.assertEqual(ttk._format_elemcreate('image', False, 'test', ('', 'a')), ("test {} a", ())) @@ -203,17 +203,17 @@ def test_format_elemcreate(self): # don't format returned values as a tcl script # minimum acceptable for vsapi self.assertEqual(ttk._format_elemcreate('vsapi', False, 'a', 'b'), - ("a b ", ())) + ('a', 'b', (), ())) # now with a state spec with multiple states self.assertEqual(ttk._format_elemcreate('vsapi', False, 'a', 'b', - ('a', 'b', 'c')), ("a b {a b} c", ())) + ('a', 'b', 'c')), ('a', 'b', ('a b', 'c'), ())) # state spec and option self.assertEqual(ttk._format_elemcreate('vsapi', False, 'a', 'b', - ('a', 'b'), opt='x'), ("a b a b", ("-opt", "x"))) + ('a', 'b'), opt='x'), ('a', 'b', ('a', 'b'), ("-opt", "x"))) # format returned values as a tcl script # state spec with a multivalue and an option self.assertEqual(ttk._format_elemcreate('vsapi', True, 'a', 'b', - ('a', 'b', [1, 2]), opt='x'), ("{a b {a b} {1 2}}", "-opt x")) + ('a', 'b', [1, 2]), opt='x'), ("a b {{a b} {1 2}}", "-opt x")) # Testing type = from # from type expects at least a type name @@ -222,9 +222,9 @@ def test_format_elemcreate(self): self.assertEqual(ttk._format_elemcreate('from', False, 'a'), ('a', ())) self.assertEqual(ttk._format_elemcreate('from', False, 'a', 'b'), - ('a', ('b', ))) + ('a', ('b',))) self.assertEqual(ttk._format_elemcreate('from', True, 'a', 'b'), - ('{a}', 'b')) + ('a', 'b')) def test_format_layoutlist(self): diff --git a/Lib/tkinter/ttk.py b/Lib/tkinter/ttk.py index efeabb7a92c627..82974e17bae178 100644 --- a/Lib/tkinter/ttk.py +++ b/Lib/tkinter/ttk.py @@ -95,40 +95,43 @@ def _format_mapdict(mapdict, script=False): def _format_elemcreate(etype, script=False, *args, **kw): """Formats args and kw according to the given element factory etype.""" - spec = None + specs = () opts = () - if etype in ("image", "vsapi"): - if etype == "image": # define an element based on an image - # first arg should be the default image name - iname = args[0] - # next args, if any, are statespec/value pairs which is almost - # a mapdict, but we just need the value - imagespec = _join(_mapdict_values(args[1:])) - spec = "%s %s" % (iname, imagespec) - + if etype == "image": # define an element based on an image + # first arg should be the default image name + iname = args[0] + # next args, if any, are statespec/value pairs which is almost + # a mapdict, but we just need the value + imagespec = (iname, *_mapdict_values(args[1:])) + if script: + specs = (imagespec,) else: - # define an element whose visual appearance is drawn using the - # Microsoft Visual Styles API which is responsible for the - # themed styles on Windows XP and Vista. - # Availability: Tk 8.6, Windows XP and Vista. - class_name, part_id = args[:2] - statemap = _join(_mapdict_values(args[2:])) - spec = "%s %s %s" % (class_name, part_id, statemap) + specs = (_join(imagespec),) + opts = _format_optdict(kw, script) + if etype == "vsapi": + # define an element whose visual appearance is drawn using the + # Microsoft Visual Styles API which is responsible for the + # themed styles on Windows XP and Vista. + # Availability: Tk 8.6, Windows XP and Vista. + class_name, part_id, *statemap = args + specs = (class_name, part_id, tuple(_mapdict_values(statemap))) opts = _format_optdict(kw, script) elif etype == "from": # clone an element # it expects a themename and optionally an element to clone from, # otherwise it will clone {} (empty element) - spec = args[0] # theme name + specs = (args[0],) # theme name if len(args) > 1: # elementfrom specified opts = (_format_optvalue(args[1], script),) if script: - spec = '{%s}' % spec + specs = _join(specs) opts = ' '.join(opts) + return specs, opts + else: + return *specs, opts - return spec, opts def _format_layoutlist(layout, indent=0, indent_size=2): """Formats a layout list so we can pass the result to ttk::style @@ -214,10 +217,10 @@ def _script_from_settings(settings): elemargs = eopts[1:argc] elemkw = eopts[argc] if argc < len(eopts) and eopts[argc] else {} - spec, opts = _format_elemcreate(etype, True, *elemargs, **elemkw) + specs, opts = _format_elemcreate(etype, True, *elemargs, **elemkw) script.append("ttk::style element create %s %s %s %s" % ( - name, etype, spec, opts)) + name, etype, specs, opts)) return '\n'.join(script) @@ -434,9 +437,9 @@ def layout(self, style, layoutspec=None): def element_create(self, elementname, etype, *args, **kw): """Create a new element in the current theme of given etype.""" - spec, opts = _format_elemcreate(etype, False, *args, **kw) + *specs, opts = _format_elemcreate(etype, False, *args, **kw) self.tk.call(self._name, "element", "create", elementname, etype, - spec, *opts) + *specs, *opts) def element_names(self): diff --git a/Misc/NEWS.d/next/Library/2023-10-27-12-46-56.gh-issue-68166.0EbWW4.rst b/Misc/NEWS.d/next/Library/2023-10-27-12-46-56.gh-issue-68166.0EbWW4.rst new file mode 100644 index 00000000000000..3dad207f5838c0 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-10-27-12-46-56.gh-issue-68166.0EbWW4.rst @@ -0,0 +1,4 @@ +Fix and document support of "vsapi" element type in +:meth:`tkinter.ttk.Style.element_create`. Add tests for ``element_create()`` +and other ``ttk.Style`` methods. Add examples for ``element_create()`` in +the documentation. From 273c73aad7df1e9a7439f3343e2cd2fb570d2d75 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 27 Oct 2023 13:29:11 +0300 Subject: [PATCH 2/8] Try to fix tests on Windows. --- Lib/test/test_ttk/test_style.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_ttk/test_style.py b/Lib/test/test_ttk/test_style.py index 3606493be7673a..75484fdfbe674b 100644 --- a/Lib/test/test_ttk/test_style.py +++ b/Lib/test/test_ttk/test_style.py @@ -123,7 +123,6 @@ def test_theme_use(self): self.style.theme_use(curr_theme) - def test_configure_custom_copy(self): style = self.style @@ -258,7 +257,7 @@ def test_element_create_vsapi_1(self): ('', 1)) style.layout('CloseButton', [('CloseButton.smallclose', {'sticky': 'news'})]) - b = ttk.Button(style='CloseButton') + b = ttk.Button(self.root, style='CloseButton') b.pack(expand=True, fill='both') self.assertEqual(b.winfo_reqwidth(), 13) self.assertEqual(b.winfo_reqheight(), 13) @@ -276,7 +275,7 @@ def test_element_create_vsapi_2(self): ('', 1)) style.layout('Explorer.Pin', [('Explorer.Pin.pin', {'sticky': 'news'})]) - pin = ttk.Checkbutton(style='Explorer.Pin') + pin = ttk.Checkbutton(self.root, style='Explorer.Pin') pin.pack(expand=True, fill='both') self.assertEqual(pin.winfo_reqwidth(), 16) self.assertEqual(pin.winfo_reqheight(), 16) @@ -291,7 +290,7 @@ def test_element_create_vsapi_3(self): ('', 1)) style.layout('Explorer.CloseButton', [('Explorer.CloseButton.headerclose', {'sticky': 'news'})]) - b = ttk.Button(style='Explorer.CloseButton') + b = ttk.Button(self.root, style='Explorer.CloseButton') b.pack(expand=True, fill='both') self.assertEqual(b.winfo_reqwidth(), 16) self.assertEqual(b.winfo_reqheight(), 16) From e6a82bad16ca02623cc523ea7511e6485e8195af Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 27 Oct 2023 13:31:41 +0300 Subject: [PATCH 3/8] Test _script_from_settings() with vsapi. --- Lib/test/test_ttk_textonly.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Lib/test/test_ttk_textonly.py b/Lib/test/test_ttk_textonly.py index 1c4113fadd0624..cbdb4da32ce5c5 100644 --- a/Lib/test/test_ttk_textonly.py +++ b/Lib/test/test_ttk_textonly.py @@ -326,6 +326,22 @@ def test_script_from_settings(self): "ttk::style element create thing image {name {state1 state2} val} " "-opt {3 2m}") + vsapi = {'pin': {'element create': + ['vsapi', 'EXPLORERBAR', 3, + ('pressed', '!selected', 3), + ('active', '!selected', 2), + ('pressed', 'selected', 6), + ('active', 'selected', 5), + ('selected', 4), + ('', 1)]}} + self.assertEqual(ttk._script_from_settings(vsapi), + "ttk::style element create pin vsapi EXPLORERBAR 3 {" + "{pressed !selected} 3 " + "{active !selected} 2 " + "{pressed selected} 6 " + "{active selected} 5 " + "selected 4 " + "{} 1} ") def test_tclobj_to_py(self): self.assertEqual( From 8cb53ccda0af29f82f834f35a1a84aa9c91769ae Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 27 Oct 2023 13:32:24 +0300 Subject: [PATCH 4/8] Add explicit root in examples. --- Doc/library/tkinter.ttk.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/library/tkinter.ttk.rst b/Doc/library/tkinter.ttk.rst index 4f47b70a26a391..41b34bf456e192 100644 --- a/Doc/library/tkinter.ttk.rst +++ b/Doc/library/tkinter.ttk.rst @@ -1423,7 +1423,7 @@ option. If you don't know the class name of a widget, use the method img1 = tkinter.PhotoImage(master=root, file='button.png') img1 = tkinter.PhotoImage(master=root, file='button-pressed.png') img1 = tkinter.PhotoImage(master=root, file='button-active.png') - style = ttk.Style() + style = ttk.Style(root) style.element_create('Button.button', 'image', img1, ('pressed', img2), ('active', img3), border=(2, 4), sticky='we') @@ -1437,7 +1437,7 @@ option. If you don't know the class name of a widget, use the method Example:: - style = ttk.Style() + style = ttk.Style(root) style.element_create('plain.background', 'from', 'default') If "vsapi" is used as the value of *etype*, :meth:`element_create` @@ -1481,7 +1481,7 @@ option. If you don't know the class name of a widget, use the method Example:: - style = ttk.Style() + style = ttk.Style(root) style.element_create('pin', 'vsapi', 'EXPLORERBAR', 3, ('pressed', '!selected', 3), ('active', '!selected', 2), From 8ab089c4b40ffb1953b873d4ac47e6891b27895e Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 28 Oct 2023 11:27:56 +0300 Subject: [PATCH 5/8] Tweak docs. --- Doc/library/tkinter.ttk.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/tkinter.ttk.rst b/Doc/library/tkinter.ttk.rst index 41b34bf456e192..9eef3e1a0cdefd 100644 --- a/Doc/library/tkinter.ttk.rst +++ b/Doc/library/tkinter.ttk.rst @@ -1392,7 +1392,7 @@ option. If you don't know the class name of a widget, use the method Create a new element in the current theme, of the given *etype* which is expected to be either "image", "from" or "vsapi". The latter is only - available in Tk 8.6a for Windows XP and Vista. + available in Tk 8.6 on Windows. If "image" is used, *args* should contain the default image name followed by statespec/value pairs (this is the imagespec), and *kw* may have the From 06a4734d0e0b40cafbca5a4275cd43f470103652 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 28 Oct 2023 16:17:12 +0300 Subject: [PATCH 6/8] Add tests for theme_create(). --- Lib/test/test_ttk/test_style.py | 146 +++++++++++++++++++++++++++++++- Lib/tkinter/ttk.py | 4 +- 2 files changed, 147 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_ttk/test_style.py b/Lib/test/test_ttk/test_style.py index 75484fdfbe674b..bff247d29faa9c 100644 --- a/Lib/test/test_ttk/test_style.py +++ b/Lib/test/test_ttk/test_style.py @@ -210,10 +210,16 @@ def test_element_create_from_errors(self): def test_element_create_image(self): style = self.style - image = tkinter.PhotoImage(master=self.root, width=10, height=10) + image = tkinter.PhotoImage(master=self.root, width=12, height=10) style.element_create('block', 'image', image) self.assertIn('block', style.element_names()) + style.layout('TestLabel1', [('block', {'sticky': 'news'})]) + a = ttk.Label(self.root, style='TestLabel1') + a.pack(expand=True, fill='both') + self.assertEqual(a.winfo_reqwidth(), 12) + self.assertEqual(a.winfo_reqheight(), 10) + imgfile = support.findfile('python.xbm', subdir='tkinterdata') img1 = tkinter.BitmapImage(master=self.root, file=imgfile, foreground='yellow', background='blue') @@ -226,6 +232,12 @@ def test_element_create_image(self): border=(2, 4), sticky='we') self.assertIn('Button.button', style.element_names()) + style.layout('Button', [('Button.button', {'sticky': 'news'})]) + b = ttk.Button(self.root, style='Button') + b.pack(expand=True, fill='both') + self.assertEqual(b.winfo_reqwidth(), 16) + self.assertEqual(b.winfo_reqheight(), 16) + def test_element_create_image_errors(self): style = self.style image = tkinter.PhotoImage(master=self.root, width=10, height=10) @@ -295,6 +307,138 @@ def test_element_create_vsapi_3(self): self.assertEqual(b.winfo_reqwidth(), 16) self.assertEqual(b.winfo_reqheight(), 16) + def test_theme_create(self): + style = self.style + curr_theme = style.theme_use() + curr_layout = style.layout('TLabel') + style.theme_create('testtheme1') + self.assertIn('testtheme1', style.theme_names()) + + style.theme_create('testtheme2', settings={ + 'elem' : {'element create': ['from', 'default'],}, + 'TLabel' : { + 'configure': {'padding': 10}, + 'layout': [('elem', {'sticky': 'we'})], + }, + }) + self.assertIn('testtheme2', style.theme_names()) + + style.theme_create('testtheme3', 'testtheme2') + self.assertIn('testtheme3', style.theme_names()) + + style.theme_use('testtheme1') + self.assertEqual(style.element_names(), ()) + self.assertEqual(style.layout('TLabel'), curr_layout) + + style.theme_use('testtheme2') + self.assertEqual(style.element_names(), ('elem',)) + self.assertEqual(style.lookup('TLabel', 'padding'), '10') + self.assertEqual(style.layout('TLabel'), [('elem', {'sticky': 'we'})]) + + style.theme_use('testtheme3') + self.assertEqual(style.element_names(), ()) + self.assertEqual(style.lookup('TLabel', 'padding'), '') + self.assertEqual(style.layout('TLabel'), [('elem', {'sticky': 'we'})]) + + style.theme_use(curr_theme) + + def test_theme_create_image(self): + style = self.style + curr_theme = style.theme_use() + image = tkinter.PhotoImage(master=self.root, width=10, height=10) + new_theme = 'testtheme4' + style.theme_create(new_theme, settings={ + 'block' : { + 'element create': ['image', image, {'width': 120, 'height': 100}], + }, + 'TestWidget.block2' : { + 'element create': ['image', image], + }, + 'TestWidget' : { + 'configure': { + 'anchor': 'left', + 'padding': (3, 0, 0, 2), + 'foreground': 'yellow', + }, + 'map': { + 'foreground': [ + ('pressed', 'red'), + ('active', 'disabled', 'blue'), + ], + }, + 'layout': [ + ('TestWidget.block', {'sticky': 'we', 'side': 'left'}), + ('TestWidget.border', { + 'sticky': 'nsw', + 'border': 1, + 'children': [ + ('TestWidget.block2', {'sticky': 'nswe'}) + ] + }) + ], + }, + }) + + style.theme_use(new_theme) + self.assertIn('block', style.element_names()) + self.assertEqual(style.lookup('TestWidget', 'anchor'), 'left') + self.assertEqual(style.lookup('TestWidget', 'padding'), '3 0 0 2') + self.assertEqual(style.lookup('TestWidget', 'foreground'), 'yellow') + self.assertEqual(style.lookup('TestWidget', 'foreground', + ['active']), 'yellow') + self.assertEqual(style.lookup('TestWidget', 'foreground', + ['active', 'pressed']), 'red') + self.assertEqual(style.lookup('TestWidget', 'foreground', + ['active', 'disabled']), 'blue') + self.assertEqual(style.layout('TestWidget'), + [ + ('TestWidget.block', {'side': 'left', 'sticky': 'we'}), + ('TestWidget.border', { + 'sticky': 'nsw', + 'border': '1', + 'children': [('TestWidget.block2', {'sticky': 'nswe'})] + }) + ]) + + b = ttk.Label(self.root, style='TestWidget') + b.pack(expand=True, fill='both') + self.assertEqual(b.winfo_reqwidth(), 134) + self.assertEqual(b.winfo_reqheight(), 100) + + style.theme_use(curr_theme) + + def test_theme_create_vsapi(self): + style = self.style + if 'xpnative' not in style.theme_names(): + self.skipTest("requires 'xpnative' theme") + curr_theme = style.theme_use() + new_theme = 'testtheme5' + style.theme_create(new_theme, settings={ + 'pin' : { + 'element create': ['vsapi', 'EXPLORERBAR', 3, + ('pressed', '!selected', 3), + ('active', '!selected', 2), + ('pressed', 'selected', 6), + ('active', 'selected', 5), + ('selected', 4), + ('', 1)], + }, + 'Explorer.Pin' : { + 'layout': [('Explorer.Pin.pin', {'sticky': 'news'})], + }, + }) + + style.theme_use(new_theme) + self.assertIn('pin', style.element_names()) + self.assertEqual(style.layout('Explorer.Pin'), []) + + pin = ttk.Checkbutton(self.root, style='Explorer.Pin') + pin.pack(expand=True, fill='both') + self.assertEqual(pin.winfo_reqwidth(), 16) + self.assertEqual(pin.winfo_reqheight(), 16) + + style.theme_use(curr_theme) + if __name__ == "__main__": unittest.main() diff --git a/Lib/tkinter/ttk.py b/Lib/tkinter/ttk.py index 82974e17bae178..66004d5ef5c3ee 100644 --- a/Lib/tkinter/ttk.py +++ b/Lib/tkinter/ttk.py @@ -217,10 +217,10 @@ def _script_from_settings(settings): elemargs = eopts[1:argc] elemkw = eopts[argc] if argc < len(eopts) and eopts[argc] else {} - specs, opts = _format_elemcreate(etype, True, *elemargs, **elemkw) + specs, eopts = _format_elemcreate(etype, True, *elemargs, **elemkw) script.append("ttk::style element create %s %s %s %s" % ( - name, etype, specs, opts)) + name, etype, specs, eopts)) return '\n'.join(script) From b1b40e2694fa90fdb0f9dc44000e7d1e843b0bee Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 28 Oct 2023 16:48:03 +0300 Subject: [PATCH 7/8] Fix tests on Windows. --- Lib/test/test_ttk/test_style.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_ttk/test_style.py b/Lib/test/test_ttk/test_style.py index bff247d29faa9c..2d838f43a3d4dd 100644 --- a/Lib/test/test_ttk/test_style.py +++ b/Lib/test/test_ttk/test_style.py @@ -430,7 +430,8 @@ def test_theme_create_vsapi(self): style.theme_use(new_theme) self.assertIn('pin', style.element_names()) - self.assertEqual(style.layout('Explorer.Pin'), []) + self.assertEqual(style.layout('Explorer.Pin'), + [('Explorer.Pin.pin', {'sticky': 'nswe'})]) pin = ttk.Checkbutton(self.root, style='Explorer.Pin') pin.pack(expand=True, fill='both') From 37967d58dc3d8966300eafd2e25b2770d6ea4ef2 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 8 Nov 2023 19:12:56 +0200 Subject: [PATCH 8/8] Add a What's New entry. --- Doc/whatsnew/3.13.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 428b648d9e1912..6b4e42f580dc5a 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -277,6 +277,11 @@ tkinter :meth:`!tk_busy_current`, and :meth:`!tk_busy_status`. (Contributed by Miguel, klappnase and Serhiy Storchaka in :gh:`72684`.) +* Add support of the "vsapi" element type in + the :meth:`~tkinter.ttk.Style.element_create` method of + :class:`tkinter.ttk.Style`. + (Contributed by Serhiy Storchaka in :gh:`68166`.) + traceback ---------