diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index 249b1602a711ad..67ecb42f0b11a3 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -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 `, + 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 `, + which provide their own :meth:`!size` method. .. method:: propagate() propagate(flag) diff --git a/Doc/library/tkinter.ttk.rst b/Doc/library/tkinter.ttk.rst index 0f5a8da1445784..fc79c7fa1845d5 100644 --- a/Doc/library/tkinter.ttk.rst +++ b/Doc/library/tkinter.ttk.rst @@ -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) diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 8abc4d0af8d19f..f8bff589495373 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -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. @@ -168,7 +178,6 @@ tkinter badge) and :meth:`~tkinter.Wm.wm_stackorder` (toplevel stacking order). (Contributed by Serhiy Storchaka in :gh:`151874`.) - xml --- diff --git a/Lib/test/test_ttk/test_widgets.py b/Lib/test/test_ttk/test_widgets.py index a3b3c88b46edd2..16f08696196634 100644 --- a/Lib/test/test_ttk/test_widgets.py +++ b/Lib/test/test_ttk/test_widgets.py @@ -2080,6 +2080,264 @@ def test_tag_has(self): 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): diff --git a/Lib/tkinter/ttk.py b/Lib/tkinter/ttk.py index cb66420d1cd129..c3a5ac160573a6 100644 --- a/Lib/tkinter/ttk.py +++ b/Lib/tkinter/ttk.py @@ -1377,7 +1377,9 @@ def move(self, item, parent, index): 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 @@ def parent(self, item): 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 index 00000000000000..3e5647cbf83549 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-22-10-30-22.gh-issue-151910.ayi8xK.rst @@ -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.