Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d27e62f
Winforms Tree widget
Oliver-Leigh Mar 10, 2026
37b17a8
Added background color change functionality
Oliver-Leigh Mar 10, 2026
c0a1e3d
Added "row path" __getitem__ to StateNode
Oliver-Leigh Mar 10, 2026
b0515a5
Fixed and simplified expand/collapse functionality
Oliver-Leigh Mar 11, 2026
cffad63
Fixed selection color when not focused.
Oliver-Leigh Mar 11, 2026
4e2ee17
Fixed get_selection
Oliver-Leigh Mar 11, 2026
1670022
Added test probe for Tree widget
Oliver-Leigh Mar 12, 2026
62cf0c3
Fixed formatting
Oliver-Leigh Mar 12, 2026
f1d90c6
Fixed regex string and enabled WinForms tree tests
Oliver-Leigh Mar 12, 2026
8ed6903
Enable multi-column icon tests for WinForms
Oliver-Leigh Mar 12, 2026
2f7c3e0
Minor style change
Oliver-Leigh Mar 12, 2026
48275da
Added change note
Oliver-Leigh Mar 12, 2026
787af46
Removed types from docstring parameters
Oliver-Leigh Mar 13, 2026
a541e4e
Changed formatting for long-argument functions
Oliver-Leigh Mar 13, 2026
b3088a0
Style change and added all columns for non-leaf
Oliver-Leigh Mar 17, 2026
8db21cf
Updated testbed for previous changes
Oliver-Leigh Mar 17, 2026
55bd421
Disabled focus rectangle
Oliver-Leigh Mar 17, 2026
20d7b0c
Added image for docs
Oliver-Leigh Mar 17, 2026
4d7156f
Merge branch 'main' into winforms-tree-widget
freakboy3742 Mar 18, 2026
c6768d7
Add a widget registration for Winforms Tree.
freakboy3742 Mar 18, 2026
aed4f3f
Add a second level for the tree data.
freakboy3742 Mar 18, 2026
708afb0
Minor code format tweak.
freakboy3742 Mar 18, 2026
09b01b6
Merge remote-tracking branch 'upstream/main' into winforms-tree-widget
freakboy3742 Mar 18, 2026
1d53f14
Removed unused methods & added 4 pragma: no cover
Oliver-Leigh Mar 19, 2026
f981178
Test selection during toggle and non-visible nodes
Oliver-Leigh Mar 19, 2026
d167905
Added tests for mouse events
Oliver-Leigh Mar 19, 2026
401284c
Removed new tests for macOS and Linux
Oliver-Leigh Mar 19, 2026
8c1ce23
Implemented focused item functionality
Oliver-Leigh Mar 21, 2026
de665ef
Minor changes and added 1 pragma no cover
Oliver-Leigh Mar 21, 2026
8c2d8e5
Updated tests to cover focus rectangle code
Oliver-Leigh Mar 22, 2026
fb15fb7
Remove expand/collapse platform specific assert.s
Oliver-Leigh Mar 22, 2026
8cc2dd2
test_mouse_events as pytest.skip on non-Windows
Oliver-Leigh Mar 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/4235.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The Tree widget is now supported in the Windows backend.
3 changes: 3 additions & 0 deletions cocoa/tests_backend/widgets/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,6 @@ async def activate_row(self, row_path):
delay=0.1,
clickCount=2,
)

async def assert_item_mouse_hover(self, row_path):
skip("Test not implemented for this platform")
1 change: 0 additions & 1 deletion docs/en/reference/data/apis_by_platform.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,6 @@ Widgets:
Tree:
description: A hierarchical tree of tabular data.
unsupported:
- winforms
- iOS
- android
- web
Expand Down
Binary file added docs/en/reference/images/tree-winforms.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 34 additions & 16 deletions examples/tree/tree/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@
"rating": "6.1",
"genre": "Animation, Adventure, Comedy",
},
{"year": 1998, "title": "Bees", "rating": "6.3", "genre": "Horror"},
{
"year": 1998,
"title": "Bees",
"rating": "6.3",
"genre": "Horror",
},
{
"year": 2007,
"title": "The Girl Who Swallowed Bees",
Expand Down Expand Up @@ -64,7 +69,9 @@ def on_select_handler(self, widget):
# Button callback functions
def insert_handler(self, widget, **kwargs):
item = choice(bee_movies)
if (year := item["year"]) >= 2000:
if (year := item["year"]) >= 2010:
root = self.decade_2010s
elif year >= 2000:
root = self.decade_2000s
elif year >= 1990:
root = self.decade_1990s
Expand Down Expand Up @@ -100,26 +107,37 @@ def startup(self):
missing_value="?",
)

self.decade_1940s = self.tree.data.append(
{"year": "1940s", "title": "", "rating": "", "genre": ""}
century_1900s = self.tree.data.append(
{"year": "2000s", "title": "20th Century", "rating": "", "genre": ""}
)
self.decade_1950s = self.tree.data.append(
{"year": "1950s", "title": "", "rating": "", "genre": ""}
century_2000s = self.tree.data.append(
{"year": "2000s", "title": "21st Century", "rating": "", "genre": ""}
)
self.decade_1960s = self.tree.data.append(
{"year": "1960s", "title": "", "rating": "", "genre": ""}

self.decade_1940s = century_1900s.append(
{"year": "1940s", "title": "Roaring 40s", "rating": "", "genre": ""}
)
self.decade_1970s = self.tree.data.append(
{"year": "1970s", "title": "", "rating": "", "genre": ""}
self.decade_1950s = century_1900s.append(
{"year": "1950s", "title": "Golden 50s", "rating": "", "genre": ""}
)
self.decade_1980s = self.tree.data.append(
{"year": "1980s", "title": "", "rating": "", "genre": ""}
self.decade_1960s = century_1900s.append(
{"year": "1960s", "title": "Swinging 60s", "rating": "", "genre": ""}
)
self.decade_1990s = self.tree.data.append(
{"year": "1990s", "title": "", "rating": "", "genre": ""}
self.decade_1970s = century_1900s.append(
{"year": "1970s", "title": "Groovy 70s", "rating": "", "genre": ""}
)
self.decade_1980s = century_1900s.append(
{"year": "1980s", "title": "Radical 80s", "rating": "", "genre": ""}
)
self.decade_1990s = century_1900s.append(
{"year": "1990s", "title": "Grunge 90s", "rating": "", "genre": ""}
)

self.decade_2000s = century_2000s.append(
{"year": "2000s", "title": "Naughty 00s", "rating": "", "genre": ""}
)
self.decade_2000s = self.tree.data.append(
{"year": "2000s", "title": "", "rating": "", "genre": ""}
self.decade_2010s = century_2000s.append(
{"year": "2010s", "title": "Modern 10s", "rating": "", "genre": ""}
)

# Buttons
Expand Down
3 changes: 3 additions & 0 deletions gtk/tests_backend/widgets/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,6 @@ async def activate_row(self, row_path):
Gtk.TreePath(row_path),
self.native_tree.get_columns()[0],
)

async def assert_item_mouse_hover(self, row_path):
pytest.skip("Test not implemented for this platform")
5 changes: 4 additions & 1 deletion qt/tests_backend/widgets/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,7 @@ async def activate_row(self, row_path):
self.native.activated.emit(index)

async def select_first_row_keyboard(self):
pytest.skip("test not implemented for this platform")
pytest.skip("Test not implemented for this platform")

async def assert_item_mouse_hover(self, row_path):
pytest.skip("Test not implemented for this platform")
140 changes: 130 additions & 10 deletions testbed/tests/widgets/test_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def source():

@pytest.fixture
async def widget(source, on_select_handler, on_activate_handler):
skip_on_platforms("iOS", "android", "windows")
skip_on_platforms("iOS", "android")
return toga.Tree(
["A", "B", "C"],
data=source,
Expand All @@ -120,7 +120,7 @@ async def widget(source, on_select_handler, on_activate_handler):

@pytest.fixture
async def headerless_widget(source, on_select_handler):
skip_on_platforms("iOS", "android", "windows")
skip_on_platforms("iOS", "android")
return toga.Tree(
columns=[
AccessorColumn(None, "a"),
Expand Down Expand Up @@ -152,7 +152,7 @@ async def headerless_probe(main_window, headerless_widget):
@pytest.fixture
async def multiselect_widget(source, on_select_handler):
# Although Android *has* a table implementation, it needs to be rebuilt.
skip_on_platforms("iOS", "android", "windows")
skip_on_platforms("iOS", "android")
return toga.Tree(
["A", "B", "C"],
data=source,
Expand All @@ -179,11 +179,7 @@ async def multiselect_probe(main_window, multiselect_widget):
test_cleanup = build_cleanup_test(
toga.Tree,
kwargs={"columns": ["A", "B", "C"]},
skip_platforms=(
"iOS",
"android",
"windows",
),
skip_platforms=("iOS", "android"),
)


Expand Down Expand Up @@ -228,6 +224,63 @@ async def test_select(widget, probe, source, on_select_handler):
await probe.redraw("A non-shortcut key was pressed")
assert widget.selection == source[1][2][0]

# Test selection remains after node with lower index is expanded
widget.collapse()
assert not probe.is_expanded(source[3])
await probe.select_row((1,))
await probe.redraw("A row that is below a row to be expanded is selected")
widget.expand(source[3])
assert widget.selection == source[1]
widget.collapse(source[3])
assert not probe.is_expanded(source[3])

# Test selection remains after the selected node is expanded
assert not probe.is_expanded(source[3])
await probe.select_row((3,))
await probe.redraw("A row that is to be expanded is selected")
widget.expand(source[3])
assert widget.selection == source[3]
widget.collapse(source[3])
assert not probe.is_expanded(source[3])

# Test selection remains after node with higher index is expanded
assert not probe.is_expanded(source[3])
await probe.select_row((4,))
await probe.redraw("A row that is to be expanded is selected")
widget.expand(source[3])
assert widget.selection == source[4]
widget.collapse(source[3])
assert not probe.is_expanded(source[3])

# Test selection remains after node with lower index is collapsed
widget.expand(source[3])
assert probe.is_expanded(source[3])
await probe.select_row((1,))
await probe.redraw("A node is expanded and a row with lower index is selected")
widget.collapse(source[3])
assert widget.selection == source[1]
assert not probe.is_expanded(source[3])

# Test selection remains after root node with higher index is collapsed
widget.expand(source[3])
assert probe.is_expanded(source[3])
await probe.select_row((4,))
await probe.redraw(
"A node is expanded and a root-row with higher index is selected"
)
widget.collapse(source[3])
assert widget.selection == source[4]
assert not probe.is_expanded(source[3])

# Exercise code for collapsing a node whose child is selected. Note that this
# behavior is inconsistent between platforms.
widget.expand(source[3])
assert probe.is_expanded(source[3])
await probe.select_row((3, 1))
await probe.redraw("A node is expanded and a child is selected")
widget.collapse(source[3])
assert not probe.is_expanded(source[3])


async def test_expand_collapse(widget, probe, source):
"""Nodes can be expanded and collapsed"""
Expand Down Expand Up @@ -461,6 +514,13 @@ async def test_expand_collapse(widget, probe, source):
assert not probe.is_expanded(source[2])
assert not probe.is_expanded(source[2][2])

# Exercise code for expanding/collapsing non-visible nodes. Note that this
# behavior is inconsistent between platforms.
widget.expand(source[0][2])
await probe.redraw("A non-visible node is expanded.")
widget.collapse(source[0][2])
await probe.redraw("A non-visible node is collapsed.")


async def test_activate(
widget,
Expand Down Expand Up @@ -913,13 +973,14 @@ async def test_cell_widget(widget, probe):
warning_check = contextlib.nullcontext()
else:
warning_check = pytest.warns(
match=".* does not support the use of widgets in cells"
match=r".* does not support the use of widgets in cells"
)

with warning_check:
widget.data = data

# Qt backend doesn't know there are widgets until the row is expanded
# Qt and Windows backends don't know there are widgets until the row is
# expanded.
await probe.expand_tree()
await probe.redraw("Tree has data with widgets")

Expand Down Expand Up @@ -988,3 +1049,62 @@ def test_deprecated_methods(widget, method_name, args, expected_args):
method(**args)

mock_method.assert_called_once_with(**expected_args)


async def test_mouse_events(widget, probe, on_activate_handler):
"""Does the widget implement mouse events correctly?"""
# These tests are needed on the Windows platform and are implemented as
# pytest.skip() on the platforms macOS, GTK and QT.

# Use some small data
small_data = [
(
{"a": "A0", "b": "", "c": ""},
[({"a": f"A0{i}", "b": i, "c": "C"}, None) for i in range(2)],
),
(
{"a": "A1", "b": "", "c": ""},
[({"a": f"A1{i}", "b": i, "c": "C"}, None) for i in range(2)],
),
]

widget.data = small_data
widget.collapse()
await probe.redraw("Tree is collapsed and awaiting hover of state-change arrow")
assert not probe.is_expanded(widget.data[0])
await probe.assert_item_mouse_hover((0,))

widget.expand(widget.data[0])
await probe.redraw("A node is expanded and awaiting hover of state-change arrow")
assert probe.is_expanded(widget.data[0])
await probe.assert_item_mouse_hover((0,))

# Simulate clicks on the state change arrow.
widget.collapse()
await probe.redraw("Tree is collapsed and awaiting toggle by mouse click")
await probe.single_click((0,), toggle=True, on_item=True)
assert widget.selection is None
await probe.single_click((0,), toggle=True, on_item=True)
assert widget.selection is None

# Simulate a normal item selection click.
await probe.redraw("Tree is collapsed and awaiting an item selection mouse click")
await probe.single_click((0,), toggle=False, on_item=True)
assert widget.selection == widget.data[0]

# Simulate a normal item selection click on a different item.
await probe.redraw("Tree is awaiting a different item selection mouse click")
await probe.single_click((1,), toggle=False, on_item=True)
assert widget.selection == widget.data[1]

# Simulate a normal selection click in the client area, but away from items.
await probe.redraw("Tree is collapsed and awaiting an item selection mouse click")
await probe.single_click((1,), toggle=False, on_item=False)
assert widget.selection is None

# Double clicking on a state-change arrow doesn't activate the row.")
await probe.double_click_state_change_arrow((0,))
on_activate_handler.assert_not_called()

# Test the mouse cursor leaving the client area
await probe.assert_mouse_leave()
2 changes: 1 addition & 1 deletion winforms/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ Switch = "toga_winforms.widgets.switch:Switch"
Table = "toga_winforms.widgets.table:Table"
TextInput = "toga_winforms.widgets.textinput:TextInput"
TimeInput = "toga_winforms.widgets.timeinput:TimeInput"
# Tree = "toga_winforms.widgets.tree:Tree"
Tree = "toga_winforms.widgets.tree:Tree"
WebView = "toga_winforms.widgets.webview:WebView"

# Windows
Expand Down
2 changes: 2 additions & 0 deletions winforms/src/toga_winforms/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from .widgets.table import Table
from .widgets.textinput import TextInput
from .widgets.timeinput import TimeInput
from .widgets.tree import Tree
from .widgets.webview import WebView
from .window import MainWindow, Window

Expand Down Expand Up @@ -85,6 +86,7 @@ def not_implemented(feature): # pragma: no cover
"Table",
"TextInput",
"TimeInput",
"Tree",
"WebView",
# Windows
"Window",
Expand Down
Loading
Loading