Skip to content

Commit aa0279b

Browse files
miss-islingtonserhiy-storchakaclaude
authored
[3.13] gh-75234: Fix keyboard selection in the lists of IDLE Settings (GH-157588) (#157931)
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 212e603) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 2ecf528 commit aa0279b

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
@@ -10,7 +10,9 @@
1010
import unittest
1111
from unittest import mock
1212
from idlelib.idle_test.mock_idle import Func
13-
from tkinter import (Tk, StringVar, IntVar, BooleanVar, DISABLED, NORMAL)
13+
from tkinter import (Tk, StringVar, IntVar, BooleanVar, DISABLED, NORMAL,
14+
EventType)
15+
from types import SimpleNamespace
1416
from idlelib import config
1517
from idlelib.configdialog import idleConf, changes, tracers
1618

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

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

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

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

1601+
def test_helplist_item_remove_keyboard_selection(self):
1602+
# gh-75234: Up and Down keys move the active item, but not the
1603+
# anchor; the handler moves the anchor.
1604+
eq = self.assertEqual
1605+
fr = self.frame
1606+
fr.helplist.delete(0, 'end')
1607+
fr.helplist.insert('end', 'name1', 'name2')
1608+
fr.helplist.selection_anchor(0)
1609+
fr.helplist.selection_set(1)
1610+
fr.helplist.activate(1)
1611+
fr.user_helplist.clear()
1612+
fr.user_helplist.extend([('name1', 'file1'), ('name2', 'file2')])
1613+
fr.set.called = fr.upc.called = 0
1614+
1615+
fr.help_source_selected(SimpleNamespace(type=EventType.KeyRelease))
1616+
eq(fr.helplist.get('anchor'), 'name2')
1617+
fr.helplist_item_remove()
1618+
eq(fr.helplist.get(0, 'end'), ('name1',))
1619+
eq(fr.user_helplist, [('name1', 'file1')])
1620+
15881621
def test_update_help_changes(self):
15891622
fr = self.frame
15901623
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)