]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-68166: Add support of "vsapi" in ttk.Style.element_create() (GH-111393)
authorSerhiy Storchaka <storchaka@gmail.com>
Mon, 27 Nov 2023 18:57:33 +0000 (20:57 +0200)
committerGitHub <noreply@github.com>
Mon, 27 Nov 2023 18:57:33 +0000 (20:57 +0200)
Doc/library/tkinter.ttk.rst
Doc/whatsnew/3.13.rst
Lib/test/test_ttk/test_style.py
Lib/test/test_ttk_textonly.py
Lib/tkinter/ttk.py
Misc/NEWS.d/next/Library/2023-11-08-18-53-07.gh-issue-68166.1iTh4Y.rst [new file with mode: 0644]

index 5fab145494498980f5da055ddf79d242f3c59a96..6e01ec7b291255236ded7ead88320cb85eaadce0 100644 (file)
@@ -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
@@ -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()
 
index ec09dfea4aad3c4fe49d7d2f55d608617d9cca10..dad49f43d9090fdc6e43e9ae73706725a256d664 100644 (file)
@@ -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
 ---------
 
index 52c4b7a0beabd037803f855548e61e076b545505..9a04a95dc40d659feba76dfea942c1107cb1f931 100644 (file)
@@ -258,6 +258,55 @@ class StyleTest(AbstractTkTest, unittest.TestCase):
         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()
@@ -358,6 +407,39 @@ class StyleTest(AbstractTkTest, unittest.TestCase):
 
         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()
index 96dc179a69ecaca79e720f0b3809768eef4ee31b..e6525c4d6c698225ca7bf2044de7d92c9a47f713 100644 (file)
@@ -179,7 +179,7 @@ class InternalFunctionsTest(unittest.TestCase):
         # 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,19 @@ class InternalFunctionsTest(unittest.TestCase):
         # 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
@@ -222,9 +224,9 @@ class InternalFunctionsTest(unittest.TestCase):
         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):
@@ -326,6 +328,22 @@ class InternalFunctionsTest(unittest.TestCase):
             "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(
index efeabb7a92c62708a63cc4b9a72a8e8070e468ba..5ca938a670831aa67ef6afd7265c8fbc18ee86b9 100644 (file)
@@ -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
@@ -214,10 +221,10 @@ def _script_from_settings(settings):
 
             elemargs = eopts[1:argc]
             elemkw = eopts[argc] if argc < len(eopts) and eopts[argc] else {}
-            specopts = _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, specopts))
+                name, etype, specs, eopts))
 
     return '\n'.join(script)
 
@@ -434,9 +441,9 @@ class Style(object):
 
     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-11-08-18-53-07.gh-issue-68166.1iTh4Y.rst b/Misc/NEWS.d/next/Library/2023-11-08-18-53-07.gh-issue-68166.1iTh4Y.rst
new file mode 100644 (file)
index 0000000..30379b8
--- /dev/null
@@ -0,0 +1,2 @@
+Add support of the "vsapi" element type in
+:meth:`tkinter.ttk.Style.element_create`.