]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-151910: Add tkinter.ttk.Treeview methods for the Tk 9.1 enhanced treeview (GH...
authorSerhiy Storchaka <storchaka@gmail.com>
Mon, 22 Jun 2026 13:08:53 +0000 (16:08 +0300)
committerGitHub <noreply@github.com>
Mon, 22 Jun 2026 13:08:53 +0000 (13:08 +0000)
Wrap the enhanced ttk::treeview widget commands added in Tk 9.1 (TIP 740),
and the detached query added in Tk 9.0, as tkinter.ttk.Treeview methods:
item navigation and queries (after_item, before_item, depth, haschildren,
visible, size, range, identifier, current), opening, hiding, sorting and
searching (expand, collapse, hide, unhide, sort, search, search_all,
search_cell, search_all_cells), cell focus, selection and tagging
(cellfocus, cellselection and its set/add/remove cell-list and *_range
rectangle forms, anchor/includes/present, tag_cell_add/remove/has), and the
detached query (detached, detached_all).

The expand() and collapse() methods, without recurse, also work on Tk older
than 9.1, where they are emulated via the item -open option.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Doc/library/tkinter.rst
Doc/library/tkinter.ttk.rst
Doc/whatsnew/3.16.rst
Lib/test/test_ttk/test_widgets.py
Lib/tkinter/ttk.py
Misc/NEWS.d/next/Library/2026-06-22-10-30-22.gh-issue-151910.ayi8xK.rst [new file with mode: 0644]

index 13fe3514b5ebc8caa8e0ba6818e3689de07ba65f..584590176220670cb6973d5bcaa8e086940c63a9 100644 (file)
@@ -1391,8 +1391,9 @@ Base and mixin classes
       ``(columns, rows)`` tuple.
 
       :meth:`size` is an alias of :meth:`!grid_size`,
-      except on the :class:`Listbox` widget,
-      which provides its own :meth:`!size` method.
+      except on :class:`Listbox` and
+      :class:`ttk.Treeview <tkinter.ttk.Treeview>`,
+      which provide their own :meth:`!size` method.
 
    .. method:: grid_slaves(row=None, column=None)
 
@@ -3291,8 +3292,9 @@ Base and mixin classes
       Same as :meth:`Misc.grid_size`: return a ``(columns, rows)`` tuple giving
       the size of the grid.
       :meth:`size` is an alias of :meth:`!grid_size`,
-      except on the :class:`Listbox` widget,
-      which provides its own :meth:`!size` method.
+      except on :class:`Listbox` and
+      :class:`ttk.Treeview <tkinter.ttk.Treeview>`,
+      which provide their own :meth:`!size` method.
 
    .. method:: propagate()
                propagate(flag)
index 0f5a8da14457840a9df9649ac589e46ae53e9064..fc79c7fa1845d5a5fd70f4cc50354798e54dcd93 100644 (file)
@@ -1192,8 +1192,9 @@ ttk.Treeview
 
    .. method:: next(item)
 
-      Returns the identifier of *item*'s next sibling, or '' if *item* is the
-      last child of its parent.
+      Returns the identifier of *item*'s next sibling,
+      or '' if *item* is the last child of its parent.
+      Equivalent to ``after_item(item, hidden=True, recurse=False)``.
 
 
    .. method:: parent(item)
@@ -1204,8 +1205,412 @@ ttk.Treeview
 
    .. method:: prev(item)
 
-      Returns the identifier of *item*'s previous sibling, or '' if *item* is
-      the first child of its parent.
+      Returns the identifier of *item*'s previous sibling,
+      or '' if *item* is the first child of its parent.
+      Equivalent to ``before_item(item, hidden=True, recurse=False)``.
+
+
+   .. method:: after_item(item, *, hidden=False, recurse=True)
+
+      Returns the identifier of the item after *item*
+      (the first child, a next sibling, or a next sibling of an ancestor),
+      or '' if there is none.
+      By default only visible items are considered;
+      if *hidden* is true, hidden items are included too.
+      If *recurse* is false, only siblings of *item* are considered
+      (see :meth:`next`).
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: before_item(item, *, hidden=False, recurse=True)
+
+      Returns the identifier of the item before *item*
+      (a previous sibling or the parent of *item*),
+      or '' if there is none.
+      By default only visible items are considered;
+      if *hidden* is true, hidden items are included too.
+      If *recurse* is false, only siblings of *item* are considered
+      (see :meth:`prev`).
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: depth(item)
+
+      Returns the number of levels between *item* and the root item.
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: haschildren(item)
+
+      Returns ``True`` if *item* has children, ``False`` otherwise.
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: visible(item)
+
+      Returns ``True`` if *item* is visible, ``False`` otherwise.
+      An item is visible if it is not detached, not hidden,
+      and all of its ancestors are open and not hidden.
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: size(item, *, hidden=False, recurse=False)
+
+      Returns the number of children of *item*.
+      If *hidden* is true, hidden items are included.
+      If *recurse* is true, all descendants of *item* are included.
+      Use ``''`` for the root item.
+      ``size(item, hidden=True)`` equals ``len(get_children(item))``
+      (which always includes hidden items).
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: range(first, last, *, hidden=False, recurse=True)
+
+      Returns a tuple of items from *first* through *last*, inclusive.
+      If *hidden* is true, hidden items are included.
+      If *recurse* is false, descendants and ancestors are excluded.
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: identifier(item, index)
+
+      Returns the identifier of the item at *index*
+      within *item*'s list of children.
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: current()
+
+      Returns the current item id and column id as a 2-tuple,
+      or an empty tuple if there is none.
+      The current item is the item under the mouse pointer.
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: expand(*items, recurse=False)
+
+      Set all of the specified items to the open state.
+      If *recurse* is true, also open all of their descendants;
+      this requires Tk 9.1.
+      Use ``''`` for the root item.
+      ``expand(item)`` is equivalent to ``item(item, open=True)``.
+
+      .. versionadded:: next
+
+
+   .. method:: collapse(*items, recurse=False)
+
+      Set all of the specified items to the closed state.
+      If *recurse* is true, also close all of their descendants;
+      this requires Tk 9.1.
+      Use ``''`` for the root item.
+      ``collapse(item)`` is equivalent to ``item(item, open=False)``.
+
+      .. versionadded:: next
+
+
+   .. method:: hide(*items, recurse=False)
+
+      Hide all of the specified items and all of their child items.
+      If *recurse* is true, also hide all of their descendants.
+      Use ``''`` for the root item.
+      ``hide(item)`` is equivalent to ``item(item, hidden=True)``.
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: unhide(*items, recurse=False)
+
+      Unhide all of the specified items.
+      If *recurse* is true, also unhide all of their descendants.
+      Use ``''`` for the root item.
+      ``unhide(item)`` is equivalent to ``item(item, hidden=False)``.
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: detached(item=None)
+
+      Returns information about detached items
+      (see :meth:`detach`).
+      Without arguments, returns a tuple of all detached items,
+      but not their descendants (see :meth:`detached_all`).
+      With *item*, returns whether *item* is detached; since Tk 9.1, also
+      returns true if an ancestor of *item* is detached.
+
+      Requires Tk 9.0 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: detached_all()
+
+      Returns a tuple of all detached items and all of their descendants
+      (see :meth:`detach`).
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: cellfocus(cell=None)
+
+      Get or set the focus cell.
+      Without *cell*, returns the focus cell as an ``(item, column)`` 2-tuple,
+      or an empty tuple if there is none.
+      With *cell*, sets the focus cell; use ``''`` to clear it.
+      A cell is specified as an ``(item, column)`` pair.
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: sort(parent, *, column=None, command=None, dictionary=False, integer=False, real=False, nocase=False, decreasing=False, ignoreempty=False, recurse=False)
+
+      Sort the children of *parent*.
+      By default the children are sorted by the value of the first display
+      column, as Unicode strings, in increasing order.
+      *column* selects the column to sort on.
+      *dictionary*, *integer* and *real* select the comparison type;
+      *nocase* makes string comparison case-insensitive.
+      *command* is a function of two values
+      returning a negative, zero or positive number.
+      *decreasing* reverses the order.
+      *ignoreempty* skips empty values (with *integer* or *real*).
+      *recurse* also sorts all descendants.
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: search(parent, pattern, *, columns=None, start=None, stop=None, dictionary=False, integer=False, real=False, nocase=False, glob=False, regexp=False, backwards=False, hidden=False, recurse=False, wraparound=False)
+
+      Search *parent*'s children for *pattern*
+      and return the identifier of the first matching item,
+      or ``''`` if there is no match.
+      By default *pattern* is matched for exact equality
+      against the value of each displayed column, as Unicode strings,
+      searching forwards through the direct children of *parent*.
+      *glob* or *regexp* select glob-style or regular expression matching;
+      *dictionary*, *integer* and *real* select the comparison type;
+      *nocase* makes it case-insensitive.
+      *columns* limits the search to the given columns.
+      *start* and *stop* bound the search;
+      *backwards* reverses its direction;
+      *wraparound* continues from the other end.
+      *hidden* also searches hidden and closed items;
+      *recurse* searches all descendants.
+
+      See :meth:`search_all`, :meth:`search_cell` and :meth:`search_all_cells`.
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: search_all(parent, pattern, **kwargs)
+
+      Like :meth:`search`,
+      but returns a tuple of the identifiers of all matching items.
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: search_cell(parent, pattern, **kwargs)
+
+      Like :meth:`search`,
+      but returns the first matching cell as an ``(item, column)`` 2-tuple,
+      or an empty tuple if there is no match.
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: search_all_cells(parent, pattern, **kwargs)
+
+      Like :meth:`search`,
+      but returns a tuple of all matching cells,
+      each an ``(item, column)`` 2-tuple.
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: cellselection()
+
+      Returns a tuple of the selected cells, each an ``(item, column)``
+      2-tuple.
+      The cell selection is independent from the item selection
+      (see :meth:`selection`).
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: cellselection_set(*cells)
+
+      The specified cells become the new cell selection.
+      Each cell is an ``(item, column)`` pair.
+      Call without arguments to clear the cell selection.
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: cellselection_add(*cells)
+
+      Add the specified cells to the cell selection.
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: cellselection_remove(*cells)
+
+      Remove the specified cells from the cell selection.
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: cellselection_set_range(first, last, *, hidden=True, recurse=True)
+
+      Set the cell selection to the rectangle of cells from *first* to *last*.
+      *first* and *last* are the opposite corner cells,
+      each an ``(item, column)`` pair, and must be in displayed columns.
+      All other cells are unselected.
+      If *hidden* is false, hidden cells are excluded;
+      if *recurse* is false, cells in descendant items are excluded.
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: cellselection_add_range(first, last, *, hidden=True, recurse=True)
+
+      Like :meth:`cellselection_set_range`,
+      but adds the rectangle of cells to the cell selection
+      instead of replacing it.
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: cellselection_remove_range(first, last, *, hidden=True, recurse=True)
+
+      Like :meth:`cellselection_set_range`,
+      but removes the rectangle of cells from the cell selection.
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: cellselection_anchor(cell=None)
+
+      Get or set the cell selection anchor.
+      Without *cell*, returns the anchor as an ``(item, column)`` 2-tuple,
+      or an empty tuple if it is unset.
+      With *cell*, sets the anchor; use ``''`` to unset it.
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: cellselection_includes(*cells)
+
+      Returns whether all of the specified cells are selected.
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: cellselection_present()
+
+      Returns whether any cell is selected.
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: tag_cell_add(tagname, *cells)
+
+      Add the given tag to each of the specified cells.
+      Each cell is an ``(item, column)`` pair.
+      Cell tags are independent from item tags.
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: tag_cell_remove(tagname, *cells)
+
+      Remove the given tag from each of the specified cells.
+      If no cell is specified, the tag is removed from all cells.
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
+
+
+   .. method:: tag_cell_has(tagname, cell=None)
+
+      Test for a cell tag, or list the cells that have it.
+      If *cell* is specified, returns whether that cell has the given tag.
+      Otherwise returns a tuple of all cells (as ``(item, column)`` 2-tuples)
+      that have the tag.
+
+      Requires Tk 9.1 or newer.
+
+      .. versionadded:: next
 
 
    .. method:: see(item)
index 3c625801c46a1cda1f6b6413e9e8bfffeae42be9..692b3cfdc067b8654ee9b0877749f1a8e2f6e57c 100644 (file)
@@ -153,6 +153,16 @@ shlex
 tkinter
 -------
 
+* Added many :class:`tkinter.ttk.Treeview` methods wrapping the enhanced
+  ``ttk::treeview`` widget commands from Tk 9.1, such as
+  :meth:`~tkinter.ttk.Treeview.sort`, :meth:`~tkinter.ttk.Treeview.search`,
+  :meth:`~tkinter.ttk.Treeview.expand`, :meth:`~tkinter.ttk.Treeview.collapse`,
+  :meth:`~tkinter.ttk.Treeview.hide`, :meth:`~tkinter.ttk.Treeview.unhide`, and
+  methods for cell focus, selection and tagging.  The
+  :meth:`~tkinter.ttk.Treeview.expand` and :meth:`~tkinter.ttk.Treeview.collapse`
+  methods (without recursion) also work on Tk older than 9.1.
+  (Contributed by Serhiy Storchaka in :gh:`151910`.)
+
 * Added new :class:`!tkinter.Text` methods :meth:`~tkinter.Text.edit_canundo`
   and :meth:`~tkinter.Text.edit_canredo` which return whether an undo or redo
   is possible.
@@ -173,7 +183,6 @@ tkinter
   badge) and :meth:`~tkinter.Wm.wm_stackorder` (toplevel stacking order).
   (Contributed by Serhiy Storchaka in :gh:`151874`.)
 
-
 xml
 ---
 
index a3b3c88b46edd2bd80b3b8621758cde84edd93db..16f0869619663454539ddb0a0c4f4ad1c222a8d2 100644 (file)
@@ -2080,6 +2080,264 @@ class TreeviewTest(AbstractWidgetTest, unittest.TestCase):
         self.assertEqual(self.tv.tag_has('tag2'), (item2,))
         self.assertEqual(self.tv.tag_has('tag3'), ())
 
+    def build_tree(self):
+        # a -> a1, a2 -> a2x; b
+        tv = self.tv
+        tv.insert('', 'end', 'a')
+        tv.insert('a', 'end', 'a1')
+        tv.insert('a', 'end', 'a2')
+        tv.insert('a2', 'end', 'a2x')
+        tv.insert('', 'end', 'b')
+        return tv
+
+    @requires_tk(9, 1)
+    def test_depth(self):
+        tv = self.build_tree()
+        self.assertEqual(tv.depth('a'), 1)
+        self.assertEqual(tv.depth('a2'), 2)
+        self.assertEqual(tv.depth('a2x'), 3)
+
+    @requires_tk(9, 1)
+    def test_haschildren(self):
+        tv = self.build_tree()
+        self.assertTrue(tv.haschildren('a'))
+        self.assertTrue(tv.haschildren('a2'))
+        self.assertFalse(tv.haschildren('a1'))
+        self.assertFalse(tv.haschildren('b'))
+
+    @requires_tk(9, 1)
+    def test_identifier(self):
+        tv = self.build_tree()
+        self.assertEqual(tv.identifier('a', 0), 'a1')
+        self.assertEqual(tv.identifier('a', 1), 'a2')
+        self.assertEqual(tv.identifier('', 0), 'a')
+        self.assertEqual(tv.identifier('', 1), 'b')
+
+    @requires_tk(9, 1)
+    def test_size(self):
+        tv = self.build_tree()
+        self.assertEqual(tv.size(''), 2)
+        self.assertEqual(tv.size('a'), 2)
+        self.assertEqual(tv.size('a', recurse=True), 3)
+        self.assertEqual(tv.size('b'), 0)
+        tv.hide('a1')
+        self.assertEqual(tv.size('a'), 1)
+        self.assertEqual(tv.size('a', hidden=True), 2)
+
+    @requires_tk(9, 1)
+    def test_range(self):
+        tv = self.build_tree()
+        tv.expand('a', recurse=True)
+        self.assertEqual(tv.range('a', 'b'), ('a', 'a1', 'a2', 'a2x', 'b'))
+        self.assertEqual(tv.range('a1', 'a2'), ('a1', 'a2'))
+        self.assertEqual(tv.range('a', 'a'), ('a',))
+        tv.hide('a1')
+        self.assertEqual(tv.range('a', 'b'), ('a', 'a2', 'a2x', 'b'))
+        self.assertEqual(tv.range('a', 'b', hidden=True),
+                         ('a', 'a1', 'a2', 'a2x', 'b'))
+
+    @requires_tk(9, 1)
+    def test_after_before_item(self):
+        tv = self.build_tree()
+        tv.expand('a', recurse=True)
+        self.assertEqual(tv.after_item('a'), 'a1')
+        self.assertEqual(tv.after_item('a1'), 'a2')
+        self.assertEqual(tv.after_item('a2x'), 'b')
+        self.assertEqual(tv.after_item('b'), '')
+        self.assertEqual(tv.before_item('b'), 'a2x')
+        self.assertEqual(tv.before_item('a1'), 'a')
+        self.assertEqual(tv.before_item('a'), '')
+        # With recurse=False the search stays at the sibling level.
+        self.assertEqual(tv.after_item('a', recurse=False), 'b')
+        self.assertEqual(tv.before_item('b', recurse=False), 'a')
+        # next/prev == after_item/before_item with hidden=True, recurse=False.
+        self.assertEqual(tv.after_item('a', hidden=True, recurse=False),
+                         tv.next('a'))
+        self.assertEqual(tv.before_item('b', hidden=True, recurse=False),
+                         tv.prev('b'))
+
+    def test_expand_collapse(self):
+        # Without recurse this works on all Tk versions (emulated before 9.1).
+        tv = self.build_tree()
+        tv.expand('a')
+        self.assertTrue(tv.item('a', 'open'))
+        self.assertFalse(tv.item('a2', 'open'))
+        tv.collapse('a')
+        self.assertFalse(tv.item('a', 'open'))
+        # Several items at once, as separate arguments or as a list.
+        tv.expand('a', 'a2')
+        self.assertTrue(tv.item('a', 'open'))
+        self.assertTrue(tv.item('a2', 'open'))
+        tv.collapse(['a', 'a2'])
+        self.assertFalse(tv.item('a', 'open'))
+        self.assertFalse(tv.item('a2', 'open'))
+
+    @requires_tk(9, 1)
+    def test_expand_collapse_recurse(self):
+        tv = self.build_tree()
+        tv.expand('a', recurse=True)
+        self.assertTrue(tv.item('a', 'open'))
+        self.assertTrue(tv.item('a2', 'open'))
+        tv.collapse('a', recurse=True)
+        self.assertFalse(tv.item('a', 'open'))
+        self.assertFalse(tv.item('a2', 'open'))
+
+    @requires_tk(9, 1)
+    def test_hide_unhide(self):
+        tv = self.build_tree()
+        tv.expand('a', recurse=True)
+        tv.hide('a1')
+        self.assertEqual(tv.range('a', 'b'), ('a', 'a2', 'a2x', 'b'))
+        tv.unhide('a1')
+        self.assertEqual(tv.range('a', 'b'), ('a', 'a1', 'a2', 'a2x', 'b'))
+        tv.hide('a1', 'a2')
+        self.assertEqual(tv.range('a', 'b'), ('a', 'b'))
+        tv.unhide(['a1', 'a2'])
+        self.assertEqual(tv.range('a', 'b'), ('a', 'a1', 'a2', 'a2x', 'b'))
+
+    @requires_tk(9, 1)
+    def test_visible(self):
+        tv = self.build_tree()
+        tv.pack()
+        self.addCleanup(tv.pack_forget)
+        self.root.update_idletasks()
+        self.assertTrue(tv.visible('a'))
+        self.assertFalse(tv.visible('a2x'))  # ancestors are closed
+        tv.expand('a', recurse=True)
+        self.root.update_idletasks()
+        self.assertTrue(tv.visible('a2x'))
+        tv.hide('a2')
+        self.root.update_idletasks()
+        self.assertFalse(tv.visible('a2x'))  # an ancestor is hidden
+
+    @requires_tk(9, 1)
+    def test_current(self):
+        tv = self.build_tree()
+        # No item is under the mouse pointer during the test.
+        self.assertEqual(tv.current(), ())
+
+    @requires_tk(9, 0)
+    def test_detached(self):
+        tv = self.build_tree()
+        self.assertEqual(tv.detached(), ())
+        self.assertFalse(tv.detached('a'))
+        tv.detach('a')
+        self.assertEqual(tv.detached(), ('a',))  # not the descendants
+        self.assertTrue(tv.detached('a'))
+        self.assertFalse(tv.detached('b'))
+        tv.move('a', '', 'end')  # reattach
+        self.assertEqual(tv.detached(), ())
+
+    @requires_tk(9, 1)
+    def test_detached_all(self):
+        # The -all form and the ancestor-aware item query require Tk 9.1.
+        tv = self.build_tree()
+        self.assertEqual(tv.detached_all(), ())
+        tv.detach('a')
+        self.assertEqual(set(tv.detached_all()),
+                         {'a', 'a1', 'a2', 'a2x'})  # with descendants
+        self.assertEqual(tv.detached(), ('a',))  # without descendants
+        self.assertTrue(tv.detached('a2x'))  # an ancestor is detached
+
+    @requires_tk(9, 1)
+    def test_cellfocus(self):
+        tv = self.create(columns=('x',))
+        tv.insert('', 'end', 'a', values=('1',))
+        self.assertEqual(tv.cellfocus(), ())
+        tv.cellfocus(('a', 'x'))
+        self.assertEqual(tv.cellfocus(), ('a', 'x'))
+        tv.cellfocus('')
+        self.assertEqual(tv.cellfocus(), ())
+
+    @requires_tk(9, 1)
+    def test_sort(self):
+        tv = self.create(columns=('x',))
+        for name, value in [('c', '3'), ('a', '1'), ('b', '2')]:
+            tv.insert('', 'end', name, values=(value,))
+        tv.sort('', column='x', integer=True)
+        self.assertEqual(tv.get_children(), ('a', 'b', 'c'))
+        tv.sort('', column='x', integer=True, decreasing=True)
+        self.assertEqual(tv.get_children(), ('c', 'b', 'a'))
+        tv.sort('', column='x', command=lambda p, q: (p > q) - (p < q))
+        self.assertEqual(tv.get_children(), ('a', 'b', 'c'))
+
+    @requires_tk(9, 1)
+    def test_search(self):
+        tv = self.create(columns=('x',))
+        for name, value in [('a', '1'), ('b', '2'), ('c', '2')]:
+            tv.insert('', 'end', name, values=(value,))
+        self.assertEqual(tv.search('', '2', columns=('x',)), 'b')
+        self.assertEqual(tv.search('', 'z', columns=('x',)), '')
+        self.assertEqual(tv.search('', '2', columns=('x',), backwards=True), 'c')
+        self.assertEqual(tv.search_all('', '2', columns=('x',)), ('b', 'c'))
+        self.assertEqual(tv.search_all('', '?', columns=('x',), glob=True),
+                         ('a', 'b', 'c'))
+
+    @requires_tk(9, 1)
+    def test_search_cell(self):
+        tv = self.create(columns=('x',))
+        for name, value in [('a', '1'), ('b', '2'), ('c', '2')]:
+            tv.insert('', 'end', name, values=(value,))
+        self.assertEqual(tv.search_cell('', '2', columns=('x',)), ('b', 'x'))
+        self.assertEqual(tv.search_cell('', 'z', columns=('x',)), ())
+        self.assertEqual(tv.search_all_cells('', '2', columns=('x',)),
+                         (('b', 'x'), ('c', 'x')))
+
+    @requires_tk(9, 1)
+    def test_cellselection(self):
+        tv = self.create(columns=('x', 'y'))
+        for name in 'ab':
+            tv.insert('', 'end', name, values=('1', '2'))
+        self.assertEqual(tv.cellselection(), ())
+        self.assertFalse(tv.cellselection_present())
+        tv.cellselection_set(('a', 'x'), ('b', 'y'))
+        self.assertEqual(set(tv.cellselection()), {('a', 'x'), ('b', 'y')})
+        self.assertTrue(tv.cellselection_present())
+        self.assertTrue(tv.cellselection_includes(('a', 'x')))
+        self.assertFalse(tv.cellselection_includes(('a', 'y')))
+        tv.cellselection_add(('a', 'y'))
+        tv.cellselection_remove(('b', 'y'))
+        self.assertEqual(set(tv.cellselection()), {('a', 'x'), ('a', 'y')})
+        # A single list of cells is also accepted.
+        tv.cellselection_set([('b', 'x'), ('b', 'y')])
+        self.assertEqual(set(tv.cellselection()), {('b', 'x'), ('b', 'y')})
+        tv.cellselection_set()  # clear
+        self.assertEqual(tv.cellselection(), ())
+        self.assertEqual(tv.cellselection_anchor(), ())
+        tv.cellselection_anchor(('a', 'x'))
+        self.assertEqual(tv.cellselection_anchor(), ('a', 'x'))
+        tv.cellselection_anchor('')
+        self.assertEqual(tv.cellselection_anchor(), ())
+
+    @requires_tk(9, 1)
+    def test_cellselection_range(self):
+        tv = self.create(columns=('x', 'y', 'z'))
+        for name in 'abc':
+            tv.insert('', 'end', name, values=('1', '2', '3'))
+        tv.cellselection_set_range(('a', 'x'), ('b', 'y'))
+        self.assertEqual(set(tv.cellselection()),
+                         {('a', 'x'), ('a', 'y'), ('b', 'x'), ('b', 'y')})
+        tv.cellselection_add_range(('c', 'z'), ('c', 'z'))
+        self.assertIn(('c', 'z'), tv.cellselection())
+        tv.cellselection_remove_range(('a', 'x'), ('b', 'x'))
+        self.assertEqual(set(tv.cellselection()),
+                         {('a', 'y'), ('b', 'y'), ('c', 'z')})
+
+    @requires_tk(9, 1)
+    def test_tag_cell(self):
+        tv = self.create(columns=('x', 'y'))
+        for name in 'ab':
+            tv.insert('', 'end', name, values=('1', '2'))
+        self.assertEqual(tv.tag_cell_has('hot'), ())
+        tv.tag_cell_add('hot', ('a', 'x'), ('b', 'y'))
+        self.assertTrue(tv.tag_cell_has('hot', ('a', 'x')))
+        self.assertFalse(tv.tag_cell_has('hot', ('a', 'y')))
+        self.assertEqual(set(tv.tag_cell_has('hot')), {('a', 'x'), ('b', 'y')})
+        tv.tag_cell_remove('hot', ('a', 'x'))
+        self.assertEqual(tv.tag_cell_has('hot'), (('b', 'y'),))
+        tv.tag_cell_remove('hot')  # from all cells
+        self.assertEqual(tv.tag_cell_has('hot'), ())
+
 
 @add_configure_tests(StandardTtkOptionsTests)
 class SeparatorTest(AbstractWidgetTest, unittest.TestCase):
index cb66420d1cd129a914bf0c3e8f48d9868c165e51..c3a5ac160573a6318bc6a6b0ab0c83ea22e69d14 100644 (file)
@@ -1377,7 +1377,9 @@ class Treeview(Widget, tkinter.XView, tkinter.YView):
 
     def next(self, item):
         """Returns the identifier of item's next sibling, or '' if item
-        is the last child of its parent."""
+        is the last child of its parent.
+
+        Equivalent to after_item(item, hidden=True, recurse=False)."""
         return self.tk.call(self._w, "next", item)
 
 
@@ -1389,10 +1391,542 @@ class Treeview(Widget, tkinter.XView, tkinter.YView):
 
     def prev(self, item):
         """Returns the identifier of item's previous sibling, or '' if
-        item is the first child of its parent."""
+        item is the first child of its parent.
+
+        Equivalent to before_item(item, hidden=True, recurse=False)."""
         return self.tk.call(self._w, "prev", item)
 
 
+    def after_item(self, item, *, hidden=False, recurse=True):
+        """Return the identifier of the item after item, or '' if there is
+        none.
+
+        The result can be the first child, a next sibling, or a next sibling
+        of an ancestor.  By default only visible items are considered; if
+        hidden is true, hidden items are included too.  If recurse is false,
+        only siblings of item are considered (next item).
+
+        * Availability: Tk 9.1"""
+        options = []
+        if hidden:
+            options.append("-hidden")
+        if not recurse:
+            options.append("-norecurse")
+        return self.tk.call(self._w, "after", *options, item)
+
+
+    def before_item(self, item, *, hidden=False, recurse=True):
+        """Return the identifier of the item before item, or '' if there is
+        none.
+
+        The result can be the previous sibling or the parent of item.  By
+        default only visible items are considered; if hidden is true, hidden
+        items are included too.  If recurse is false, only siblings of item
+        are considered (previous item).
+
+        * Availability: Tk 9.1"""
+        options = []
+        if hidden:
+            options.append("-hidden")
+        if not recurse:
+            options.append("-norecurse")
+        return self.tk.call(self._w, "before", *options, item)
+
+
+    def depth(self, item):
+        """Return the number of levels between item and the root item.
+
+        * Availability: Tk 9.1"""
+        return self.tk.getint(self.tk.call(self._w, "depth", item))
+
+
+    def haschildren(self, item):
+        """Return True if item has children, False otherwise.
+
+        * Availability: Tk 9.1"""
+        return self.tk.getboolean(self.tk.call(self._w, "haschildren", item))
+
+
+    def visible(self, item):
+        """Return True if item is visible, False otherwise.
+
+        An item is visible if it is not detached, not hidden, and all of its
+        ancestors are open and not hidden.
+
+        * Availability: Tk 9.1"""
+        return self.tk.getboolean(self.tk.call(self._w, "visible", item))
+
+
+    def size(self, item, *, hidden=False, recurse=False):  # overrides Misc.size
+        """Return the number of children of item.
+
+        If hidden is true, hidden items are included.  If recurse is true,
+        all descendants of item are included.  Use '' for the root item.
+        size(item, hidden=True) equals len(get_children(item)).
+
+        * Availability: Tk 9.1"""
+        options = []
+        if hidden:
+            options.append("-hidden")
+        if recurse:
+            options.append("-recurse")
+        return self.tk.getint(self.tk.call(self._w, "size", *options, item))
+
+
+    def range(self, first, last, *, hidden=False, recurse=True):
+        """Return a tuple of items from first through last, inclusive.
+
+        If hidden is true, hidden items are included.  If recurse is false,
+        descendants and ancestors are excluded.
+
+        * Availability: Tk 9.1"""
+        options = []
+        if hidden:
+            options.append("-hidden")
+        if not recurse:
+            options.append("-norecurse")
+        return self.tk.splitlist(
+            self.tk.call(self._w, "range", *options, first, last))
+
+
+    def identifier(self, item, index):
+        """Return the identifier of the item at index within item's list of
+        children.
+
+        * Availability: Tk 9.1"""
+        return self.tk.call(self._w, "identifier", item, index)
+
+
+    def current(self):
+        """Return the current item id and column id as a 2-tuple, or an empty
+        tuple if there is none.
+
+        The current item is the item under the mouse pointer.
+
+        * Availability: Tk 9.1"""
+        return self.tk.splitlist(self.tk.call(self._w, "current"))
+
+
+    def _itemlist(self, command, items, recurse):
+        if len(items) == 1 and isinstance(items[0], (tuple, list)):
+            items = items[0]
+        options = ("-recurse",) if recurse else ()
+        self.tk.call(self._w, command, *options, items)
+
+
+    def expand(self, *items, recurse=False):
+        """Set all of the specified items to the open state.
+
+        If recurse is true, also open all of their descendants; this requires
+        Tk 9.1.  Use '' for the root item.
+
+        expand(item) is equivalent to item(item, open=True)."""
+        try:
+            self._itemlist("expand", items, recurse)
+        except tkinter.TclError:
+            if recurse or self.info_patchlevel() >= (9, 1):
+                raise
+            self._open(True, items)
+
+
+    def collapse(self, *items, recurse=False):
+        """Set all of the specified items to the closed state.
+
+        If recurse is true, also close all of their descendants; this requires
+        Tk 9.1.  Use '' for the root item.
+
+        collapse(item) is equivalent to item(item, open=False)."""
+        try:
+            self._itemlist("collapse", items, recurse)
+        except tkinter.TclError:
+            if recurse or self.info_patchlevel() >= (9, 1):
+                raise
+            self._open(False, items)
+
+
+    def _open(self, opening, items):
+        if len(items) == 1 and isinstance(items[0], (tuple, list)):
+            items = items[0]
+        for item in items:
+            if item != '':  # the root item has no open state
+                self.item(item, open=opening)
+
+
+    def hide(self, *items, recurse=False):
+        """Hide all of the specified items and all of their child items.
+
+        If recurse is true, also hide all of their descendants.  Use '' for
+        the root item.  hide(item) is equivalent to item(item, hidden=True).
+
+        * Availability: Tk 9.1"""
+        self._itemlist("hide", items, recurse)
+
+
+    def unhide(self, *items, recurse=False):
+        """Unhide all of the specified items.
+
+        If recurse is true, also unhide all of their descendants.  Use '' for
+        the root item.  unhide(item) is equivalent to item(item, hidden=False).
+
+        * Availability: Tk 9.1"""
+        self._itemlist("unhide", items, recurse)
+
+
+    def detached(self, item=None):
+        """Return all detached items, or whether item is detached.
+
+        Without arguments, return a tuple of all detached items (but not
+        their descendants; see detached_all).  With item, return whether item
+        is detached; since Tk 9.1, also return true if an ancestor of item
+        is detached.
+
+        * Availability: Tk 9.0"""
+        if item is None:
+            return self.tk.splitlist(self.tk.call(self._w, "detached"))
+        return self.tk.getboolean(self.tk.call(self._w, "detached", item))
+
+
+    def detached_all(self):
+        """Return a tuple of all detached items and all of their descendants.
+
+        * Availability: Tk 9.1"""
+        return self.tk.splitlist(self.tk.call(self._w, "detached", "-all"))
+
+
+    def cellfocus(self, cell=None):
+        """Get or set the focus cell.
+
+        Without cell, return the focus cell as an (item, column) 2-tuple, or
+        an empty tuple if there is none.  With cell, set the focus cell; use
+        '' to clear it.  A cell is specified as an (item, column) pair.
+
+        * Availability: Tk 9.1"""
+        if cell is None:
+            return self.tk.splitlist(self.tk.call(self._w, "cellfocus"))
+        return self.tk.call(self._w, "cellfocus", cell)
+
+
+    def sort(self, parent, *, column=None, command=None, dictionary=False,
+             integer=False, real=False, nocase=False, decreasing=False,
+             ignoreempty=False, recurse=False):
+        """Sort the children of parent.
+
+        By default the children are sorted by the value of the first display
+        column, as Unicode strings, in increasing order.  column selects the
+        column to sort on.  dictionary, integer and real select the
+        comparison type; nocase makes string comparison case-insensitive.
+        command is a function of two values returning a negative, zero or
+        positive number.  decreasing reverses the order.  ignoreempty skips
+        empty values (with integer or real).  recurse also sorts all
+        descendants.
+
+        * Availability: Tk 9.1"""
+        options = []
+        if column is not None:
+            options += ("-column", column)
+        if dictionary:
+            options.append("-dictionary")
+        if integer:
+            options.append("-integer")
+        if real:
+            options.append("-real")
+        if nocase:
+            options.append("-nocase")
+        if decreasing:
+            options.append("-decreasing")
+        if ignoreempty:
+            options.append("-ignoreempty")
+        if recurse:
+            options.append("-recurse")
+        if command is None:
+            self.tk.call(self._w, "sort", parent, *options)
+        else:
+            cmd = self.register(command)
+            try:
+                self.tk.call(self._w, "sort", parent, *options,
+                             "-command", cmd)
+            finally:
+                self.deletecommand(cmd)
+
+
+    def search(self, parent, pattern, *, columns=None, start=None, stop=None,
+               dictionary=False, integer=False, real=False, nocase=False,
+               glob=False, regexp=False, backwards=False, hidden=False,
+               recurse=False, wraparound=False):
+        """Search parent's children for pattern and return the first match.
+
+        By default pattern is matched for exact equality against the value of
+        each displayed column, as Unicode strings, searching forwards through
+        the direct children of parent.  glob or regexp select glob-style or
+        regular expression matching; dictionary, integer and real select the
+        comparison type; nocase makes it case-insensitive.  columns limits the
+        search to the given columns.  start and stop bound the search;
+        backwards reverses its direction; wraparound continues from the other
+        end.  hidden also searches hidden and closed items; recurse searches
+        all descendants.
+
+        Return the identifier of the first matching item, or '' if there is no
+        match.  See search_all (all matching items), search_cell (the first
+        matching cell) and search_all_cells (all matching cells).
+
+        * Availability: Tk 9.1"""
+        return self._search(False, False, parent, pattern, columns, start,
+                            stop, dictionary, integer, real, nocase, glob,
+                            regexp, backwards, hidden, recurse, wraparound)
+
+
+    def search_all(self, parent, pattern, *, columns=None, start=None,
+                   stop=None, dictionary=False, integer=False, real=False,
+                   nocase=False, glob=False, regexp=False, backwards=False,
+                   hidden=False, recurse=False, wraparound=False):
+        """Search parent's children for pattern and return all matches.
+
+        Like search, but returns a tuple of the identifiers of all matching
+        items.
+
+        * Availability: Tk 9.1"""
+        return self._search(True, False, parent, pattern, columns, start,
+                            stop, dictionary, integer, real, nocase, glob,
+                            regexp, backwards, hidden, recurse, wraparound)
+
+
+    def search_cell(self, parent, pattern, *, columns=None, start=None,
+                    stop=None, dictionary=False, integer=False, real=False,
+                    nocase=False, glob=False, regexp=False, backwards=False,
+                    hidden=False, recurse=False, wraparound=False):
+        """Search parent's children for pattern and return the first cell.
+
+        Like search, but matches and returns a cell, as an (item, column)
+        2-tuple, or () if there is no match.
+
+        * Availability: Tk 9.1"""
+        return self._search(False, True, parent, pattern, columns, start,
+                            stop, dictionary, integer, real, nocase, glob,
+                            regexp, backwards, hidden, recurse, wraparound)
+
+
+    def search_all_cells(self, parent, pattern, *, columns=None, start=None,
+                         stop=None, dictionary=False, integer=False,
+                         real=False, nocase=False, glob=False, regexp=False,
+                         backwards=False, hidden=False, recurse=False,
+                         wraparound=False):
+        """Search parent's children for pattern and return all matching cells.
+
+        Like search, but returns a tuple of all matching cells, each an
+        (item, column) 2-tuple.
+
+        * Availability: Tk 9.1"""
+        return self._search(True, True, parent, pattern, columns, start,
+                            stop, dictionary, integer, real, nocase, glob,
+                            regexp, backwards, hidden, recurse, wraparound)
+
+
+    def _search(self, all, cell, parent, pattern, columns, start, stop,
+                dictionary, integer, real, nocase, glob, regexp, backwards,
+                hidden, recurse, wraparound):
+        options = []
+        if columns is not None:
+            options += ("-columns", columns)
+        if start is not None:
+            options += ("-start", start)
+        if stop is not None:
+            options += ("-stop", stop)
+        if dictionary:
+            options.append("-dictionary")
+        if integer:
+            options.append("-integer")
+        if real:
+            options.append("-real")
+        if nocase:
+            options.append("-nocase")
+        if glob:
+            options.append("-glob")
+        if regexp:
+            options.append("-regexp")
+        if backwards:
+            options.append("-backwards")
+        if hidden:
+            options.append("-hidden")
+        if recurse:
+            options.append("-recurse")
+        if wraparound:
+            options.append("-wraparound")
+        if cell:
+            options.append("-cell")
+        if all:
+            options.append("-all")
+        res = self.tk.call(self._w, "search", parent, *options, pattern)
+        if cell:
+            cells = tuple(self.tk.splitlist(c)
+                          for c in self.tk.splitlist(res))
+            if all:
+                return cells
+            return cells[0] if cells else ()
+        if all:
+            return self.tk.splitlist(res)
+        return res
+
+
+    @staticmethod
+    def _cells(cells):
+        # Accept either several (item, column) cells or a single list of them.
+        if (len(cells) == 1 and cells[0]
+                and isinstance(cells[0], (tuple, list))
+                and isinstance(cells[0][0], (tuple, list))):
+            return cells[0]
+        return cells
+
+
+    def cellselection(self):
+        """Return a tuple of the selected cells.
+
+        Each cell is an (item, column) 2-tuple.  The cell selection is
+        independent from the item selection (see selection).
+
+        * Availability: Tk 9.1"""
+        return tuple(self.tk.splitlist(c) for c in
+                     self.tk.splitlist(self.tk.call(self._w, "cellselection")))
+
+
+    def _cellselection(self, selop, cells):
+        self.tk.call(self._w, "cellselection", selop, self._cells(cells))
+
+
+    def cellselection_set(self, *cells):
+        """The specified cells become the new cell selection.
+
+        Each cell is an (item, column) pair.  Call without arguments to clear
+        the cell selection.
+
+        * Availability: Tk 9.1"""
+        self._cellselection("set", cells)
+
+
+    def cellselection_add(self, *cells):
+        """Add the specified cells to the cell selection.
+
+        * Availability: Tk 9.1"""
+        self._cellselection("add", cells)
+
+
+    def cellselection_remove(self, *cells):
+        """Remove the specified cells from the cell selection.
+
+        * Availability: Tk 9.1"""
+        self._cellselection("remove", cells)
+
+
+    def _cellselection_range(self, selop, first, last, hidden, recurse):
+        options = []
+        if not hidden:
+            options.append("-nohidden")
+        if not recurse:
+            options.append("-norecurse")
+        self.tk.call(self._w, "cellselection", selop, *options, first, last)
+
+
+    def cellselection_set_range(self, first, last, *, hidden=True,
+                                recurse=True):
+        """Set the cell selection to the rectangle of cells from first to last.
+
+        first and last are the opposite corner cells, each an (item, column)
+        pair, and must be in displayed columns.  All other cells are
+        unselected.  If hidden is false, hidden cells are excluded; if recurse
+        is false, cells in descendant items are excluded.
+
+        * Availability: Tk 9.1"""
+        self._cellselection_range("set", first, last, hidden, recurse)
+
+
+    def cellselection_add_range(self, first, last, *, hidden=True,
+                                recurse=True):
+        """Add the rectangle of cells from first to last to the cell selection.
+
+        Like cellselection_set_range, but adds to the selection instead of
+        replacing it.
+
+        * Availability: Tk 9.1"""
+        self._cellselection_range("add", first, last, hidden, recurse)
+
+
+    def cellselection_remove_range(self, first, last, *, hidden=True,
+                                   recurse=True):
+        """Remove the rectangle of cells from first to last from the selection.
+
+        Like cellselection_set_range, but removes the cells from the selection.
+
+        * Availability: Tk 9.1"""
+        self._cellselection_range("remove", first, last, hidden, recurse)
+
+
+    def cellselection_anchor(self, cell=None):
+        """Get or set the cell selection anchor.
+
+        Without cell, return the anchor as an (item, column) 2-tuple, or an
+        empty tuple if unset.  With cell, set the anchor; use '' to unset it.
+
+        * Availability: Tk 9.1"""
+        if cell is None:
+            return self.tk.splitlist(
+                self.tk.call(self._w, "cellselection", "anchor"))
+        return self.tk.call(self._w, "cellselection", "anchor", cell)
+
+
+    def cellselection_includes(self, *cells):
+        """Return whether all of the specified cells are selected.
+
+        * Availability: Tk 9.1"""
+        return self.tk.getboolean(self.tk.call(
+            self._w, "cellselection", "includes", self._cells(cells)))
+
+
+    def cellselection_present(self):
+        """Return whether any cell is selected.
+
+        * Availability: Tk 9.1"""
+        return self.tk.getboolean(
+            self.tk.call(self._w, "cellselection", "present"))
+
+
+    def tag_cell_add(self, tagname, *cells):
+        """Add the given tag to each of the specified cells.
+
+        Each cell is an (item, column) pair.  Cell tags are independent from
+        item tags (see tag_add).
+
+        * Availability: Tk 9.1"""
+        self.tk.call(self._w, "tag", "cell", "add", tagname, self._cells(cells))
+
+
+    def tag_cell_remove(self, tagname, *cells):
+        """Remove the given tag from each of the specified cells.
+
+        If no cell is specified, the tag is removed from all cells.
+
+        * Availability: Tk 9.1"""
+        if cells:
+            self.tk.call(self._w, "tag", "cell", "remove", tagname,
+                         self._cells(cells))
+        else:
+            # Omit the cell list entirely (an empty list would match no cell).
+            self.tk.call(self._w, "tag", "cell", "remove", tagname)
+
+
+    def tag_cell_has(self, tagname, cell=None):
+        """Test for a cell tag, or list the cells that have it.
+
+        If cell is specified, return whether that cell has the given tag.
+        Otherwise return a tuple of all cells (as (item, column) 2-tuples)
+        that have the tag.
+
+        * Availability: Tk 9.1"""
+        if cell is None:
+            return tuple(self.tk.splitlist(c) for c in self.tk.splitlist(
+                self.tk.call(self._w, "tag", "cell", "has", tagname)))
+        return self.tk.getboolean(
+            self.tk.call(self._w, "tag", "cell", "has", tagname, cell))
+
+
     def see(self, item):
         """Ensure that item is visible.
 
diff --git a/Misc/NEWS.d/next/Library/2026-06-22-10-30-22.gh-issue-151910.ayi8xK.rst b/Misc/NEWS.d/next/Library/2026-06-22-10-30-22.gh-issue-151910.ayi8xK.rst
new file mode 100644 (file)
index 0000000..3e5647c
--- /dev/null
@@ -0,0 +1,6 @@
+Added :class:`tkinter.ttk.Treeview` methods wrapping the enhanced
+``ttk::treeview`` widget commands added in Tk 9.1 (and the ``detached`` query
+added in Tk 9.0): item navigation and queries, opening, hiding, sorting and
+searching of items, and cell focus, selection and tagging.  The
+:meth:`~tkinter.ttk.Treeview.expand` and :meth:`~tkinter.ttk.Treeview.collapse`
+methods (without recursion) also work on Tk older than 9.1.