https://github.com/python/cpython/commit/3a5247ccdca31b8da8a7dc8ebc2bd3e81abe459d
commit: 3a5247ccdca31b8da8a7dc8ebc2bd3e81abe459d
branch: 3.15
author: Miss Islington (bot) <[email protected]>
committer: hugovk <[email protected]>
date: 2026-09-28T19:00:22+03:00
summary:

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

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 a63fdfeec2f723..c5c9872501c056 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