Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a handler for ListView.SearchForVirtualItem in winforms backend for keyboard navigation in tables and detailed lists #2956

Merged
merged 11 commits into from
Nov 20, 2024
Prev Previous commit
Next Next commit
reorganize test, attempt fixing missing coverage, implement macos tab…
…le keyboard test
samtupy committed Nov 18, 2024
commit 1e16b9ca7525aa1550197d1aabb8dcea54be4333
3 changes: 3 additions & 0 deletions android/tests_backend/widgets/table.py
Original file line number Diff line number Diff line change
@@ -94,3 +94,6 @@ def typeface(self):
@property
def text_size(self):
return self._row_view(0).getChildAt(0).getTextSize()

async def assert_keyboard_navigation(self):
pytest.skip("test not implemented for this platform")
2 changes: 2 additions & 0 deletions cocoa/tests_backend/widgets/base.py
Original file line number Diff line number Diff line change
@@ -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,
26 changes: 26 additions & 0 deletions cocoa/tests_backend/widgets/table.py
Original file line number Diff line number Diff line change
@@ -144,3 +144,29 @@ async def activate_row(self, row):
delay=0.1,
clickCount=2,
)

async def assert_keyboard_navigation(self):
# Insure that the list has keyboard focus
self.native_table.window.makeFirstResponder(self.native_table)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to use this call, rather than self.widget.focus()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, because as I'd previously mentioned, toga.Table.focus() is currently a no-op for some reason, it's been explicitly disabled in core/src/toga/widgets/table.py

    def focus(self) -> None:
        """No-op; Table cannot accept input focus."""
        pass

I was planning to open an issue about this as I, as well, don't understand why table.focus has been made into a no-op, it makes it so I can't focus my users on a list programmatically which I wanted to do since a giant list was the primary control of my app.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah - my apologies - I forgot you'd raised that.

I can't give you a good answer for why it's been disabled; my guess is that there might have been some odd behavior related to selection handling. That's definitely worth a standalone issue (and investigation); if you can fix it, that would definitely be welcome. I can't see any fundamental reason why a table shouldn't be able to be given focus, as long as the behaviour otherwise well defined.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also - that issue doesn't need to be fixed to land this PR; but it might be worth dropping a comment on the "pseudo-focus" statements highlighting that they could be replaced with self.widget.focus() once the issue is resolved (adding a reference to the new ticket number for that issue).

await self.redraw(f"Table is focused sel {self.native_table.selectedRow}")
# Navigate down then up, with a letter then 2 arrow keys.
await self.type_character("a")
await self.redraw("First row is selected")
assert self.native_table.selectedRow == 0
await self.type_character("<down>")
await self.redraw("Second row is selected")
assert self.native_table.selectedRow == 1
await self.type_character("<up>")
await self.redraw("First row is selected")
assert self.native_table.selectedRow == 0

# Move down 3 rows, with letter, arrow, then letter.
await self.type_character("a")
await self.redraw("Second row is selected")
assert self.native_table.selectedRow == 1
await self.type_character("<down>")
await self.redraw("Third row is selected")
assert self.native_table.selectedRow == 2
await self.type_character("a")
await self.redraw("Forth row is selected")
assert self.native_table.selectedRow == 3
3 changes: 3 additions & 0 deletions gtk/tests_backend/widgets/table.py
Original file line number Diff line number Diff line change
@@ -84,3 +84,6 @@ async def activate_row(self, row):
Gtk.TreePath(row),
self.native_table.get_columns()[0],
)

async def assert_keyboard_navigation(self):
pytest.skip("test not implemented for this platform")
21 changes: 1 addition & 20 deletions testbed/tests/widgets/test_table.py
Original file line number Diff line number Diff line change
@@ -161,26 +161,7 @@ async def test_scroll(widget, probe):

async def test_keyboard_navigation(widget, source, probe):
"""The list can be navigated using a keyboard."""
if toga.platform.current_platform != "windows":
pytest.skip("test only applies on windows at present")
# Focus the list by pressing tab.
await probe.type_character("\t")

# Navigate 2 items down. In this dataset, all items start with "A".
await probe.type_character("a")
await probe.type_character("a")
await probe.redraw("Third row is selected")
assert widget.selection == source[2]

# Select the last item with the end key.
await probe.type_character("<end>")
await probe.redraw("Last row is selected")
assert widget.selection == source[-1]

# Navigate by 1 item, wrapping around.
await probe.type_character("a")
await probe.redraw("First row is selected")
assert widget.selection == source[0]
await probe.assert_keyboard_navigation()


async def test_select(widget, probe, source, on_select_handler):
13 changes: 9 additions & 4 deletions winforms/src/toga_winforms/widgets/table.py
Original file line number Diff line number Diff line change
@@ -123,8 +123,11 @@ def winforms_search_for_virtual_item(self, sender, e):
while True:
# Either winforms might provide a starting index out of bounds if searching at list borders,
# or this loop may travel out of bounds itself while searching. In either case, wrap around.
if i < 0:
i = len(self._data) - 1
if i < 0: # pragma: no cover
# This could theoretically happen if this event is fired with a backwards search direction,
# however this edgecase should not take place within Toga's intended use of this event.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor style tweak - we try to keep comments to ~80 chars, allowing up to 88 at a stretch if it helps with flow.

Copy link
Contributor Author

@samtupy samtupy Nov 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay, this one is stumping me because I very rarely contribute to projects coded by sighted people and thus I haven't run into this restriction before. I'll work out a way to stylize comments such as by writing a program that reads clipboard input, trims all whitespace then reports on the line length so I know whether the comment needs further shortening without spamming the right arrow key 80 times and slowly counting, removing one word then doing it again etc. I won't be able to write comments for Toga's codebase until I've come up with an accessible solution or another blind person tells me one, and none I know who might know are around at the second.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for double comment, just remembered to ask. Is this 80 chars per comment or 80 chars per line, I'm guessing per-comment?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The limit is 80 characters per line of code, including any leading space. It's essentially a holdover from old teletype programming - you should be able to see the entire line of code on an 80x25 text mode terminal screen, with no line wrapping. We've now got much larger screens that aren't constrained by 80 columns, but visual readability decreases with line length, so it's now a "soft 80" limit.

The fact that this isn't an consideration for non-sighted programmers is a fascinating insight - thanks for letting me know about that.

As far as automated tooling goes - most sighted IDEs (e.g., Visual Studio) can do an "auto word-wrap" on comments; I'm not sure what your tools will allow. At the very least, the flake8 configuration should be catching this, but I've just noticed that our flake8 configuration sets a line length of 119 (the config is in tox.ini for... reasons), so we're not flagging > 88 character lines. That's something we should probably fix - I've opened #2975 to track this improvement.

In the meantime, I definitely don't want this to be an impediment to your ability to contribute to Toga. If you can't find an automated solution, I'm more than happy to do the last bit of leg work and re-flow long comments. I often end up tweaking comments in PRs anyway (purely for language style and content reasons), so it's not a major imposition.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we standardized on 88 rather than 80, for the reasons explained here? It's better to be consistent, so we don't end up in the situation we had before when code was being reformatted every time it moved from one developer to another.

In VS Code I use this extension.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The limit that is hard-enforced is definitely 88; I still think of it as a "soft 80". Make of that what you will 😀

And yes - I use that VSCode tooling as well; the issue is that the PR author is blind, and AFAIK isn't using Visual Studio.

# i = len(self._data) - 1
raise NotImplementedError("backwards search unsupported")
elif i >= len(self._data):
i = 0
if (
@@ -135,8 +138,10 @@ def winforms_search_for_virtual_item(self, sender, e):
):
found_item = True
break
if find_previous:
i -= 1
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:
20 changes: 20 additions & 0 deletions winforms/tests_backend/widgets/table.py
Original file line number Diff line number Diff line change
@@ -99,3 +99,23 @@ async def activate_row(self, row):
delta=0,
)
)

async def assert_keyboard_navigation(self):
# Focus the list by pressing tab.
await self.type_character("\t")

# Navigate 2 items down. In this dataset, all items start with "A".
await self.type_character("a")
await self.type_character("a")
await self.redraw("Third row is selected")
assert self.native.Items[2].Selected

# Select the last item with the end key.
await self.type_character("<end>")
await self.redraw("Last row is selected")
assert self.native.Items[self.row_count - 1].Selected

# Navigate by 1 item, wrapping around.
await self.type_character("a")
await self.redraw("First row is selected")
assert self.native.Items[0].Selected