Skip to content

Commit

Permalink
gh-68166: Add support of "vsapi" in ttk.Style.element_create() (GH-11…
Browse files Browse the repository at this point in the history
  • Loading branch information
serhiy-storchaka authored Nov 27, 2023
1 parent 45d6485 commit 4dcfd02
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 32 deletions.
60 changes: 59 additions & 1 deletion Doc/library/tkinter.ttk.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1391,7 +1391,8 @@ option. If you don't know the class name of a widget, use the method
.. method:: element_create(elementname, etype, *args, **kw)

Create a new element in the current theme, of the given *etype* which is
expected to be either "image" or "from".
expected to be either "image", "from" or "vsapi".
The latter is only 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
Expand Down Expand Up @@ -1439,6 +1440,63 @@ option. If you don't know the class name of a widget, use the method
style = ttk.Style(root)
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 an optional sequence
of 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(root)
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')

.. versionchanged:: 3.13
Added support of the "vsapi" element factory.

.. method:: element_names()

Expand Down
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,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
---------

Expand Down
82 changes: 82 additions & 0 deletions Lib/test/test_ttk/test_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,55 @@ def test_element_create_image_errors(self):
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(self.root, 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(self.root, 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(self.root, style='Explorer.CloseButton')
b.pack(expand=True, fill='both')
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()
Expand Down Expand Up @@ -358,6 +407,39 @@ def test_theme_create_image(self):

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'),
[('Explorer.Pin.pin', {'sticky': 'nswe'})])

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()
32 changes: 25 additions & 7 deletions Lib/test/test_ttk_textonly.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", ()))
Expand All @@ -203,17 +203,19 @@ 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', ('', 1), ()))
# 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"))
opt='x'), ("a b {{} 1}", "-opt x"))
self.assertEqual(ttk._format_elemcreate('vsapi', True, 'a', 'b',
[('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
Expand All @@ -222,9 +224,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):
Expand Down Expand Up @@ -326,6 +328,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(
Expand Down
55 changes: 31 additions & 24 deletions Lib/tkinter/ttk.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,40 +95,47 @@ 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.
if len(args) < 3:
class_name, part_id = args
statemap = (((), 1),)
else:
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
Expand Down Expand Up @@ -214,10 +221,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, eopts = _format_elemcreate(etype, True, *elemargs, **elemkw)

script.append("ttk::style element create %s %s %s %s" % (
name, etype, spec, opts))
name, etype, specs, eopts))

return '\n'.join(script)

Expand Down Expand Up @@ -434,9 +441,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):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add support of the "vsapi" element type in
:meth:`tkinter.ttk.Style.element_create`.

0 comments on commit 4dcfd02

Please sign in to comment.