From 093c25a546081c2349114409ddad21229326bd60 Mon Sep 17 00:00:00 2001 From: sobitoks Date: Tue, 21 Jul 2026 13:13:55 +0200 Subject: [PATCH 1/2] Focus first empty field in credentials dialog When some credentials are already stored, put the keyboard focus on the first empty field so that e.g. a saved username and password let the user type the OTP code straight away without clicking. A field counts as empty both when it was never stored (None) and when it was stored blank (''). --- src/openvpn3_indicator/dialogs/credentials.py | 5 ++ src/openvpn3_indicator/tests/credentials.py | 56 ++++++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/openvpn3_indicator/dialogs/credentials.py b/src/openvpn3_indicator/dialogs/credentials.py index a68bc0b..3959d86 100644 --- a/src/openvpn3_indicator/dialogs/credentials.py +++ b/src/openvpn3_indicator/dialogs/credentials.py @@ -54,6 +54,7 @@ def construct_credentials_dialog(name, user_inputs, allow_store=True, on_connect entries = list() default_store = False can_store = False + focus_entry = None for user_input in user_inputs: label = Gtk.Label(label=user_input.name, hexpand=True, xalign=0, margin_right=10) grid.attach(label, 0, row, 1, 1) @@ -65,6 +66,8 @@ def construct_credentials_dialog(name, user_inputs, allow_store=True, on_connect if user_input.value is not None: entry.set_text(user_input.value) default_store = True + if not entry.get_text() and focus_entry is None: + focus_entry = entry entry.set_activates_default(True) grid.attach(entry, 1, row, 1, 1) entries.append(entry) @@ -108,6 +111,8 @@ def on_schedule(): default.set_can_default(True) default.grab_default() dialog.show_all() + if focus_entry is not None: + focus_entry.grab_focus() if remain_active is not None: GLib.timeout_add(1000, on_schedule) return dialog diff --git a/src/openvpn3_indicator/tests/credentials.py b/src/openvpn3_indicator/tests/credentials.py index 67f41e2..c6720a2 100755 --- a/src/openvpn3_indicator/tests/credentials.py +++ b/src/openvpn3_indicator/tests/credentials.py @@ -11,6 +11,58 @@ from openvpn3_indicator.about import APPLICATION_ID from openvpn3_indicator.dialogs.credentials import construct_credentials_dialog, CredentialsUserInput +# name, mask, value, can_store, and the field we expect to end up focused. +# '' means there's a saved value that's just empty. None means nothing was ever saved. +# OTP is never saved, so it's always None. +FOCUS_SCENARIOS = [ + ('nothing stored', [ + CredentialsUserInput('Username', False, None, True), + CredentialsUserInput('Password', True, None, True), + CredentialsUserInput('OTP', True, None, False), + ], 'Username'), + ('username stored only', [ + CredentialsUserInput('Username', False, 'user', True), + CredentialsUserInput('Password', True, '', True), + CredentialsUserInput('OTP', True, None, False), + ], 'Password'), + ('password stored only', [ + CredentialsUserInput('Username', False, '', True), + CredentialsUserInput('Password', True, 'secret', True), + CredentialsUserInput('OTP', True, None, False), + ], 'Username'), + ('username and password stored', [ + CredentialsUserInput('Username', False, 'user', True), + CredentialsUserInput('Password', True, 'secret', True), + CredentialsUserInput('OTP', True, None, False), + ], 'OTP'), +] + + +def focused_field_label(dialog): + focused = dialog.get_focus() + grid = next((child for child in dialog.get_content_area().get_children() + if isinstance(child, Gtk.Grid)), None) + if not isinstance(focused, Gtk.Entry) or grid is None: + return None + row = grid.child_get_property(focused, 'top-attach') + for child in grid.get_children(): + if (isinstance(child, Gtk.Label) + and grid.child_get_property(child, 'left-attach') == 0 + and grid.child_get_property(child, 'top-attach') == row): + return child.get_text() + return None + + +def verify_focus(): + for name, user_inputs, expected in FOCUS_SCENARIOS: + dialog = construct_credentials_dialog(name, user_inputs) + while Gtk.events_pending(): # realize widgets so the focus is applied + Gtk.main_iteration_do(False) + got = focused_field_label(dialog) + print(f"[{'PASS' if got == expected else 'FAIL'}] {name}: focus on {got!r} (expected {expected!r})") + dialog.destroy() + + class Test(Gtk.Application): def __init__(self): Gtk.Application.__init__(self, @@ -24,12 +76,14 @@ def on_activate(self, *args, **kwargs): def on_startup(self, *args, **kwargs): self.hold() + verify_focus() + user_inputs = [ CredentialsUserInput(name='Username', mask=False, value='user', can_store=True), CredentialsUserInput(name='Password', mask=True, value=None, can_store=True), CredentialsUserInput(name='Other', mask=True, value='rehto', can_store=True), ] - + dialog = construct_credentials_dialog('Test', user_inputs, on_connect=self.action_connect, on_cancel=self.action_quit) dialog.set_visible(True) From cd92eb44e7793988fe66a73f241c9855a6ef5017 Mon Sep 17 00:00:00 2001 From: sobitoks Date: Thu, 23 Jul 2026 09:45:59 +0200 Subject: [PATCH 2/2] Use elif with a truthiness check for the empty field Reads cleaner than two separate ifs, and `if user_input.value:` treats both None and an empty string '' as empty, so a field stored blank still counts as empty when picking which one to focus. --- src/openvpn3_indicator/dialogs/credentials.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/openvpn3_indicator/dialogs/credentials.py b/src/openvpn3_indicator/dialogs/credentials.py index 3959d86..2b7efa2 100644 --- a/src/openvpn3_indicator/dialogs/credentials.py +++ b/src/openvpn3_indicator/dialogs/credentials.py @@ -63,10 +63,10 @@ def construct_credentials_dialog(name, user_inputs, allow_store=True, on_connect entry.set_visibility(False) if user_input.can_store: can_store = True - if user_input.value is not None: + if user_input.value: entry.set_text(user_input.value) default_store = True - if not entry.get_text() and focus_entry is None: + elif focus_entry is None: focus_entry = entry entry.set_activates_default(True) grid.attach(entry, 1, row, 1, 1)