Skip to content

Commit 3a5247c

Browse files
miss-islingtonserhiy-storchakaclaude
authored
[3.15] gh-75234: Fix keyboard selection in the lists of IDLE Settings (GH-157588) (#157929)
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent f76c430 commit 3a5247c

3 files changed

Lines changed: 69 additions & 15 deletions

File tree

‎Lib/idlelib/configdialog.py‎

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1146,8 +1146,8 @@ def create_page_keys(self):
11461146
selected keyset. The keybindings are loaded in load_keys_list()
11471147
and are pairs of (event, [keys]) where keys can be a list
11481148
of one or more key combinations to bind to the same event.
1149-
Mouse button 1 click invokes on_bindingslist_select(), which
1150-
allows button_new_keys to be clicked.
1149+
Mouse button 1 click or Up or Down key invokes
1150+
on_bindingslist_select(), which allows button_new_keys to be clicked.
11511151
11521152
So, an item is selected in listbindings, which activates
11531153
button_new_keys, and clicking button_new_keys calls function
@@ -1221,9 +1221,12 @@ def create_page_keys(self):
12211221
scroll_target_y = Scrollbar(frame_target)
12221222
scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL)
12231223
self.bindingslist = Listbox(
1224-
frame_target, takefocus=FALSE, exportselection=FALSE)
1224+
frame_target, takefocus=True, exportselection=FALSE)
12251225
self.bindingslist.bind('<ButtonRelease-1>',
12261226
self.on_bindingslist_select)
1227+
self.bindingslist.bind('<KeyRelease-Up>', self.on_bindingslist_select)
1228+
self.bindingslist.bind('<KeyRelease-Down>',
1229+
self.on_bindingslist_select)
12271230
scroll_target_y['command'] = self.bindingslist.yview
12281231
scroll_target_x['command'] = self.bindingslist.xview
12291232
self.bindingslist['yscrollcommand'] = scroll_target_y.set
@@ -1427,7 +1430,14 @@ def save_as_new_key_set(self):
14271430
self.create_new_key_set(new_keys_name)
14281431

14291432
def on_bindingslist_select(self, event):
1430-
"Activate button to assign new keys to selected action."
1433+
"""Activate button to assign new keys to selected action.
1434+
1435+
Event can result from either mouse click or Up or Down key.
1436+
The keys move the selection, but not the anchor used by
1437+
get_new_keys and var_changed_keybinding.
1438+
"""
1439+
if event.type.name == 'KeyRelease':
1440+
self.bindingslist.selection_anchor(ACTIVE)
14311441
self.button_new_keys.state(('!disabled',))
14321442

14331443
def create_new_key_set(self, new_key_set_name):
@@ -1465,9 +1475,8 @@ def load_keys_list(self, keyset_name):
14651475
14661476
An action/key binding can be selected to change the key binding.
14671477
"""
1468-
reselect = False
1478+
list_index = 0
14691479
if self.bindingslist.curselection():
1470-
reselect = True
14711480
list_index = self.bindingslist.index(ANCHOR)
14721481
keyset = idleConf.GetKeySet(keyset_name)
14731482
# 'set' is dict mapping virtual event to list of key events.
@@ -1482,10 +1491,11 @@ def load_keys_list(self, keyset_name):
14821491
if bind_name in changes['keys'][keyset_name]:
14831492
key = changes['keys'][keyset_name][bind_name]
14841493
self.bindingslist.insert(END, bind_name+' - '+key)
1485-
if reselect:
1486-
self.bindingslist.see(list_index)
1487-
self.bindingslist.select_set(list_index)
1488-
self.bindingslist.select_anchor(list_index)
1494+
self.bindingslist.see(list_index)
1495+
self.bindingslist.select_set(list_index)
1496+
self.bindingslist.select_anchor(list_index)
1497+
self.bindingslist.activate(list_index)
1498+
self.button_new_keys.state(('!disabled',))
14891499

14901500
@staticmethod
14911501
def save_new_key_set(keyset_name, keyset):
@@ -2124,6 +2134,8 @@ def create_frame_help(self):
21242134
scroll_helplist['command'] = self.helplist.yview
21252135
self.helplist['yscrollcommand'] = scroll_helplist.set
21262136
self.helplist.bind('<ButtonRelease-1>', self.help_source_selected)
2137+
self.helplist.bind('<KeyRelease-Up>', self.help_source_selected)
2138+
self.helplist.bind('<KeyRelease-Down>', self.help_source_selected)
21272139

21282140
frame_buttons = Frame(self)
21292141
self.button_helplist_edit = Button(
@@ -2146,7 +2158,14 @@ def create_frame_help(self):
21462158
self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5)
21472159

21482160
def help_source_selected(self, event):
2149-
"Handle event for selecting additional help."
2161+
"""Handle event for selecting additional help.
2162+
2163+
Event can result from either mouse click or Up or Down key.
2164+
The keys move the selection, but not the anchor used by
2165+
helplist_item_edit and helplist_item_remove.
2166+
"""
2167+
if event.type.name == 'KeyRelease':
2168+
self.helplist.selection_anchor(ACTIVE)
21502169
self.set_add_delete_state()
21512170

21522171
def set_add_delete_state(self):

‎Lib/idlelib/idle_test/test_configdialog.py‎

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
import unittest
1010
from unittest import mock
1111
from idlelib.idle_test.mock_idle import Func
12-
from tkinter import (Tk, StringVar, IntVar, BooleanVar, DISABLED, NORMAL)
12+
from tkinter import (Tk, StringVar, IntVar, BooleanVar, DISABLED, NORMAL,
13+
EventType)
14+
from types import SimpleNamespace
1315
from idlelib import config
1416
from idlelib.configdialog import idleConf, changes, tracers
1517

@@ -1060,6 +1062,14 @@ def test_on_bindingslist_select(self):
10601062
self.assertEqual(b.get('anchor'), 'find')
10611063
self.assertNotIn('disabled', d.button_new_keys.state())
10621064

1065+
# gh-75234: Up and Down keys move the active item, but not the
1066+
# anchor; the handler moves the anchor.
1067+
d.button_new_keys.state(('disabled',))
1068+
b.activate(0)
1069+
d.on_bindingslist_select(SimpleNamespace(type=EventType.KeyRelease))
1070+
self.assertEqual(b.get('anchor'), 'copy')
1071+
self.assertNotIn('disabled', d.button_new_keys.state())
1072+
10631073
def test_create_new_key_set_and_save_new_key_set(self):
10641074
eq = self.assertEqual
10651075
d = self.page
@@ -1110,11 +1120,14 @@ def test_load_keys_list(self):
11101120
'force-open-completions - <Control-Key-space>',
11111121
'spam - <Shift-Key-a>')
11121122

1113-
# No current selection.
1123+
# No current selection: select the first item.
1124+
d.button_new_keys.state(('disabled',))
11141125
d.load_keys_list('my keys')
11151126
eq(b.get(0, 'end'), expected)
1116-
eq(b.get('anchor'), '')
1117-
eq(b.curselection(), ())
1127+
eq(b.get('anchor'), 'copy - <Control-Key-c> <Control-Key-C>')
1128+
eq(b.curselection(), (0, ))
1129+
eq(b.index('active'), 0)
1130+
self.assertNotIn('disabled', d.button_new_keys.state())
11181131

11191132
# Check selection.
11201133
b.selection_set(1)
@@ -1584,6 +1597,26 @@ def test_helplist_item_remove(self):
15841597
eq(fr.user_helplist, [])
15851598
self.assertTrue(fr.upc.called == fr.set.called == 1)
15861599

1600+
def test_helplist_item_remove_keyboard_selection(self):
1601+
# gh-75234: Up and Down keys move the active item, but not the
1602+
# anchor; the handler moves the anchor.
1603+
eq = self.assertEqual
1604+
fr = self.frame
1605+
fr.helplist.delete(0, 'end')
1606+
fr.helplist.insert('end', 'name1', 'name2')
1607+
fr.helplist.selection_anchor(0)
1608+
fr.helplist.selection_set(1)
1609+
fr.helplist.activate(1)
1610+
fr.user_helplist.clear()
1611+
fr.user_helplist.extend([('name1', 'file1'), ('name2', 'file2')])
1612+
fr.set.called = fr.upc.called = 0
1613+
1614+
fr.help_source_selected(SimpleNamespace(type=EventType.KeyRelease))
1615+
eq(fr.helplist.get('anchor'), 'name2')
1616+
fr.helplist_item_remove()
1617+
eq(fr.helplist.get(0, 'end'), ('name1',))
1618+
eq(fr.user_helplist, [('name1', 'file1')])
1619+
15871620
def test_update_help_changes(self):
15881621
fr = self.frame
15891622
self.addCleanup(setattr, fr, 'update_help_changes', Func()) # Re-mask method.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix editing help sources and key bindings in the IDLE Settings dialog after
2+
selecting them with the keyboard.

0 commit comments

Comments
 (0)