diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py index 4c94d9be69e95e..a5d1aaeb5a946f 100644 --- a/Lib/idlelib/configdialog.py +++ b/Lib/idlelib/configdialog.py @@ -1146,8 +1146,8 @@ def create_page_keys(self): selected keyset. The keybindings are loaded in load_keys_list() and are pairs of (event, [keys]) where keys can be a list of one or more key combinations to bind to the same event. - Mouse button 1 click invokes on_bindingslist_select(), which - allows button_new_keys to be clicked. + Mouse button 1 click or Up or Down key invokes + on_bindingslist_select(), which allows button_new_keys to be clicked. So, an item is selected in listbindings, which activates button_new_keys, and clicking button_new_keys calls function @@ -1221,9 +1221,12 @@ def create_page_keys(self): scroll_target_y = Scrollbar(frame_target) scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL) self.bindingslist = Listbox( - frame_target, takefocus=FALSE, exportselection=FALSE) + frame_target, takefocus=True, exportselection=FALSE) self.bindingslist.bind('', self.on_bindingslist_select) + self.bindingslist.bind('', self.on_bindingslist_select) + self.bindingslist.bind('', + self.on_bindingslist_select) scroll_target_y['command'] = self.bindingslist.yview scroll_target_x['command'] = self.bindingslist.xview self.bindingslist['yscrollcommand'] = scroll_target_y.set @@ -1427,7 +1430,14 @@ def save_as_new_key_set(self): self.create_new_key_set(new_keys_name) def on_bindingslist_select(self, event): - "Activate button to assign new keys to selected action." + """Activate button to assign new keys to selected action. + + Event can result from either mouse click or Up or Down key. + The keys move the selection, but not the anchor used by + get_new_keys and var_changed_keybinding. + """ + if event.type.name == 'KeyRelease': + self.bindingslist.selection_anchor(ACTIVE) self.button_new_keys.state(('!disabled',)) def create_new_key_set(self, new_key_set_name): @@ -1465,9 +1475,8 @@ def load_keys_list(self, keyset_name): An action/key binding can be selected to change the key binding. """ - reselect = False + list_index = 0 if self.bindingslist.curselection(): - reselect = True list_index = self.bindingslist.index(ANCHOR) keyset = idleConf.GetKeySet(keyset_name) # 'set' is dict mapping virtual event to list of key events. @@ -1482,10 +1491,11 @@ def load_keys_list(self, keyset_name): if bind_name in changes['keys'][keyset_name]: key = changes['keys'][keyset_name][bind_name] self.bindingslist.insert(END, bind_name+' - '+key) - if reselect: - self.bindingslist.see(list_index) - self.bindingslist.select_set(list_index) - self.bindingslist.select_anchor(list_index) + self.bindingslist.see(list_index) + self.bindingslist.select_set(list_index) + self.bindingslist.select_anchor(list_index) + self.bindingslist.activate(list_index) + self.button_new_keys.state(('!disabled',)) @staticmethod def save_new_key_set(keyset_name, keyset): @@ -2124,6 +2134,8 @@ def create_frame_help(self): scroll_helplist['command'] = self.helplist.yview self.helplist['yscrollcommand'] = scroll_helplist.set self.helplist.bind('', self.help_source_selected) + self.helplist.bind('', self.help_source_selected) + self.helplist.bind('', self.help_source_selected) frame_buttons = Frame(self) self.button_helplist_edit = Button( @@ -2146,7 +2158,14 @@ def create_frame_help(self): self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5) def help_source_selected(self, event): - "Handle event for selecting additional help." + """Handle event for selecting additional help. + + Event can result from either mouse click or Up or Down key. + The keys move the selection, but not the anchor used by + helplist_item_edit and helplist_item_remove. + """ + if event.type.name == 'KeyRelease': + self.helplist.selection_anchor(ACTIVE) self.set_add_delete_state() def set_add_delete_state(self): diff --git a/Lib/idlelib/idle_test/test_configdialog.py b/Lib/idlelib/idle_test/test_configdialog.py index 21bdf36b52f451..12927bee8d31fb 100644 --- a/Lib/idlelib/idle_test/test_configdialog.py +++ b/Lib/idlelib/idle_test/test_configdialog.py @@ -10,7 +10,9 @@ import unittest from unittest import mock from idlelib.idle_test.mock_idle import Func -from tkinter import (Tk, StringVar, IntVar, BooleanVar, DISABLED, NORMAL) +from tkinter import (Tk, StringVar, IntVar, BooleanVar, DISABLED, NORMAL, + EventType) +from types import SimpleNamespace from idlelib import config from idlelib.configdialog import idleConf, changes, tracers @@ -1061,6 +1063,14 @@ def test_on_bindingslist_select(self): self.assertEqual(b.get('anchor'), 'find') self.assertNotIn('disabled', d.button_new_keys.state()) + # gh-75234: Up and Down keys move the active item, but not the + # anchor; the handler moves the anchor. + d.button_new_keys.state(('disabled',)) + b.activate(0) + d.on_bindingslist_select(SimpleNamespace(type=EventType.KeyRelease)) + self.assertEqual(b.get('anchor'), 'copy') + self.assertNotIn('disabled', d.button_new_keys.state()) + def test_create_new_key_set_and_save_new_key_set(self): eq = self.assertEqual d = self.page @@ -1111,11 +1121,14 @@ def test_load_keys_list(self): 'force-open-completions - ', 'spam - ') - # No current selection. + # No current selection: select the first item. + d.button_new_keys.state(('disabled',)) d.load_keys_list('my keys') eq(b.get(0, 'end'), expected) - eq(b.get('anchor'), '') - eq(b.curselection(), ()) + eq(b.get('anchor'), 'copy - ') + eq(b.curselection(), (0, )) + eq(b.index('active'), 0) + self.assertNotIn('disabled', d.button_new_keys.state()) # Check selection. b.selection_set(1) @@ -1585,6 +1598,26 @@ def test_helplist_item_remove(self): eq(fr.user_helplist, []) self.assertTrue(fr.upc.called == fr.set.called == 1) + def test_helplist_item_remove_keyboard_selection(self): + # gh-75234: Up and Down keys move the active item, but not the + # anchor; the handler moves the anchor. + eq = self.assertEqual + fr = self.frame + fr.helplist.delete(0, 'end') + fr.helplist.insert('end', 'name1', 'name2') + fr.helplist.selection_anchor(0) + fr.helplist.selection_set(1) + fr.helplist.activate(1) + fr.user_helplist.clear() + fr.user_helplist.extend([('name1', 'file1'), ('name2', 'file2')]) + fr.set.called = fr.upc.called = 0 + + fr.help_source_selected(SimpleNamespace(type=EventType.KeyRelease)) + eq(fr.helplist.get('anchor'), 'name2') + fr.helplist_item_remove() + eq(fr.helplist.get(0, 'end'), ('name1',)) + eq(fr.user_helplist, [('name1', 'file1')]) + def test_update_help_changes(self): fr = self.frame self.addCleanup(setattr, fr, 'update_help_changes', Func()) # Re-mask method. diff --git a/Misc/NEWS.d/next/IDLE/2026-09-15-20-14-08.gh-issue-75234.O8ZNFV.rst b/Misc/NEWS.d/next/IDLE/2026-09-15-20-14-08.gh-issue-75234.O8ZNFV.rst new file mode 100644 index 00000000000000..70d64aa09a6dee --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2026-09-15-20-14-08.gh-issue-75234.O8ZNFV.rst @@ -0,0 +1,2 @@ +Fix editing help sources and key bindings in the IDLE Settings dialog after +selecting them with the keyboard.