https://github.com/python/cpython/commit/e700dbdaf89aebf51c12d5e856fb26c3cda9b48c
commit: e700dbdaf89aebf51c12d5e856fb26c3cda9b48c
branch: 3.14
author: Miss Islington (bot) <[email protected]>
committer: terryjreedy <[email protected]>
date: 2026-09-21T21:27:28Z
summary:

[3.14] gh-75234: Fix keyboard selection in the lists of IDLE Settings 
(GH-157588) (#157930)

gh-75234: Fix keyboard selection in the lists of IDLE Settings (GH-157588)

The Up and Down keys move the selection in the help sources and key
bindings lists, but not the anchor. So the buttons that act on the
selected item stayed disabled, and acted on the anchored item instead
of the selected one. Move the anchor on the key events, as the font
list already does.

---------
(cherry picked from commit 212e6035133957a66f1a823e012b4dc2a5158ce8)

Co-authored-by: Serhiy Storchaka <[email protected]>
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>

files:
A Misc/NEWS.d/next/IDLE/2026-09-15-20-14-08.gh-issue-75234.O8ZNFV.rst
M Lib/idlelib/configdialog.py
M Lib/idlelib/idle_test/test_configdialog.py

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('<ButtonRelease-1>',
                                self.on_bindingslist_select)
+        self.bindingslist.bind('<KeyRelease-Up>', self.on_bindingslist_select)
+        self.bindingslist.bind('<KeyRelease-Down>',
+                               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('<ButtonRelease-1>', self.help_source_selected)
+        self.helplist.bind('<KeyRelease-Up>', self.help_source_selected)
+        self.helplist.bind('<KeyRelease-Down>', 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 3c5f99f98f0bc2..367c63dc01126a 100644
--- a/Lib/idlelib/idle_test/test_configdialog.py
+++ b/Lib/idlelib/idle_test/test_configdialog.py
@@ -9,7 +9,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
 
@@ -1060,6 +1062,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
@@ -1110,11 +1120,14 @@ def test_load_keys_list(self):
                     'force-open-completions - <Control-Key-space>',
                     'spam - <Shift-Key-a>')
 
-        # 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 - <Control-Key-c> <Control-Key-C>')
+        eq(b.curselection(), (0, ))
+        eq(b.index('active'), 0)
+        self.assertNotIn('disabled', d.button_new_keys.state())
 
         # Check selection.
         b.selection_set(1)
@@ -1584,6 +1597,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.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to