Skip to content

Commit

Permalink
Merge pull request #2956 from samtupy/winforms-table-search
Browse files Browse the repository at this point in the history
Add a handler for ListView.SearchForVirtualItem in winforms backend for keyboard navigation in tables and detailed lists
  • Loading branch information
freakboy3742 authored Nov 20, 2024
2 parents eea1b98 + 59bee96 commit 3fb4fe3
Show file tree
Hide file tree
Showing 9 changed files with 128 additions and 13 deletions.
3 changes: 3 additions & 0 deletions android/tests_backend/widgets/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,6 @@ def typeface(self):
@property
def text_size(self):
return self._row_view(0).getChildAt(0).getTextSize()

async def acquire_keyboard_focus(self):
pytest.skip("test not implemented for this platform")
1 change: 1 addition & 0 deletions changes/2956.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Multi-letter keyboard navigation in Tables and DetailedLists with the winforms backend is now functional.
2 changes: 2 additions & 0 deletions cocoa/tests_backend/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ async def type_character(self, char, modifierFlags=0):
key_code = {
"<backspace>": 51,
"<esc>": 53,
"<down>": 125,
"<up>": 126,
" ": 49,
"\n": 36,
"a": 0,
Expand Down
9 changes: 9 additions & 0 deletions cocoa/tests_backend/widgets/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class TableProbe(SimpleProbe):
native_class = NSScrollView
supports_icons = 2 # All columns
supports_keyboard_shortcuts = True
supports_keyboard_boundary_shortcuts = False
supports_widgets = True

def __init__(self, widget):
Expand Down Expand Up @@ -144,3 +145,11 @@ async def activate_row(self, row):
delay=0.1,
clickCount=2,
)

async def acquire_keyboard_focus(self):
self.native_table.window.makeFirstResponder(
self.native_table
) # switch to widget.focus() when possible (#2972).
# Insure first row is selected.
await self.type_character("<down>")
await self.type_character("<up>")
3 changes: 3 additions & 0 deletions gtk/tests_backend/widgets/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,6 @@ async def activate_row(self, row):
Gtk.TreePath(row),
self.native_table.get_columns()[0],
)

async def acquire_keyboard_focus(self):
pytest.skip("test not implemented for this platform")
44 changes: 44 additions & 0 deletions testbed/tests/widgets/test_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,50 @@ async def test_scroll(widget, probe):
assert -100 < probe.scroll_position <= 0


async def test_keyboard_navigation(widget, source, probe):
"""The list can be navigated using a keyboard."""
await probe.acquire_keyboard_focus()
await probe.redraw("First row selected")
assert widget.selection == widget.data[0]

# Navigate down with letter, arrow, letter.
await probe.type_character("a")
await probe.redraw("Letter pressed - second row selected")
assert widget.selection == widget.data[1]
await probe.type_character("<down>")
await probe.redraw("Down arrow pressed - third row selected")
assert widget.selection == widget.data[2]
await probe.type_character("a")
await probe.redraw("Letter pressed - forth row selected")
assert widget.selection == widget.data[3]

# Select the last item with the end key if supported then wrap around.
if probe.supports_keyboard_boundary_shortcuts:
await probe.type_character("<end>")
await probe.redraw("Last row is selected")
assert widget.selection == widget.data[-1]
# Navigate by 1 item, wrapping around.
await probe.type_character("a")
await probe.redraw("Letter pressed - first row is selected")
else:
await probe.type_character("<up>")
await probe.type_character("<up>")
await probe.type_character("<up>")
await probe.redraw("Up arrow pressed thrice - first row is selected")
assert widget.selection == widget.data[0]

# Type a letter that no items start with to verify the selection doesn't change.
await probe.type_character("x")
await probe.redraw("Invalid letter pressed - first row is still selected")
assert widget.selection == widget.data[0]

# clear the table and verify with an empty selection.
widget.data.clear()
await probe.type_character("a")
await probe.redraw("Letter pressed - no row selected")
assert not widget.selection


async def test_select(widget, probe, source, on_select_handler):
"""Rows can be selected"""
# Initial selection is empty
Expand Down
70 changes: 58 additions & 12 deletions winforms/src/toga_winforms/widgets/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ def create(self):
self.native.CacheVirtualItems += WeakrefCallable(
self.winforms_cache_virtual_items
)
self.native.SearchForVirtualItem += WeakrefCallable(
self.winforms_search_for_virtual_item
)
self.native.VirtualItemsSelectionRangeChanged += WeakrefCallable(
self.winforms_item_selection_changed
)
Expand Down Expand Up @@ -108,6 +111,49 @@ def winforms_cache_virtual_items(self, sender, e):
for i in range(new_length):
self._cache.append(self._new_item(i + self._first_item))

def winforms_search_for_virtual_item(self, sender, e):
if (
not e.IsTextSearch or not self._accessors or not self._data
): # pragma: no cover
# If this list is empty, or has no columns, or it's an unsupported search
# type, there's no search to be done. These situation are difficult to
# trigger in CI; they're here as a safety catch.
return
find_previous = e.Direction in [
WinForms.SearchDirectionHint.Up,
WinForms.SearchDirectionHint.Left,
]
i = e.StartIndex
found_item = False
while True:
# It is possible for e.StartIndex to be received out-of-range if the user
# performs keyboard navigation at its edge, so check before accessing data
if i < 0: # pragma: no cover
# This could happen if this event is fired searching backwards,
# however this should not happen in Toga's use of it.
# i = len(self._data) - 1
raise NotImplementedError("backwards search unsupported")
elif i >= len(self._data):
i = 0
if (
self._item_text(self._data[i], self._accessors[0])[
: len(e.Text)
].lower()
== e.Text.lower()
):
found_item = True
break
if find_previous: # pragma: no cover
# Toga does not currently need backwards searching functionality.
# i -= 1
raise NotImplementedError("backwards search unsupported")
else:
i += 1
if i == e.StartIndex:
break
if found_item:
e.Index = i

def winforms_item_selection_changed(self, sender, e):
self.interface.on_select()

Expand Down Expand Up @@ -156,19 +202,8 @@ def icon(attr):

return None if icon is None else icon._impl

def text(attr):
val = getattr(item, attr, None)
if isinstance(val, toga.Widget):
warn("Winforms does not support the use of widgets in cells")
val = None
if isinstance(val, tuple):
val = val[1]
if val is None:
val = self.interface.missing_value
return str(val)

lvi = WinForms.ListViewItem(
[text(attr) for attr in self._accessors],
[self._item_text(item, attr) for attr in self._accessors],
)

# If the table has accessors, populate the icons for the table.
Expand All @@ -181,6 +216,17 @@ def text(attr):

return lvi

def _item_text(self, item, attr):
val = getattr(item, attr, None)
if isinstance(val, toga.Widget):
warn("Winforms does not support the use of widgets in cells")
val = None
if isinstance(val, tuple):
val = val[1]
if val is None:
val = self.interface.missing_value
return str(val)

def _image_index(self, icon):
images = self.native.SmallImageList.Images
key = str(icon.path)
Expand Down
2 changes: 1 addition & 1 deletion winforms/tests_backend/probe.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

KEY_CODES = {
f"<{name}>": f"{{{name.upper()}}}"
for name in ["esc", "up", "down", "left", "right"]
for name in ["esc", "up", "down", "left", "right", "home", "end"]
}
KEY_CODES.update(
{
Expand Down
7 changes: 7 additions & 0 deletions winforms/tests_backend/widgets/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class TableProbe(SimpleProbe):
background_supports_alpha = False
supports_icons = 1 # First column only
supports_keyboard_shortcuts = False
supports_keyboard_boundary_shortcuts = True
supports_widgets = False

@property
Expand Down Expand Up @@ -99,3 +100,9 @@ async def activate_row(self, row):
delta=0,
)
)

async def acquire_keyboard_focus(self):
await self.type_character(
"\t"
) # switch to widget.focus() when possible (#2972)
await self.type_character(" ") # select first row

0 comments on commit 3fb4fe3

Please sign in to comment.