Hand out hotkey bindings as a snapshot

Hotkeys.all() returned its synchronised list, and every reader had to
lock it while iterating: a lock owned by one class and taken by four
others. It now returns a copy, and the lock stays inside Hotkeys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 10:36:43 +00:00
parent eeccb495eb
commit 5b1809cc2d
5 changed files with 23 additions and 31 deletions

View File

@@ -88,22 +88,20 @@ public final class HotkeyEngine implements GlobalInputHook.Listener {
return; return;
} }
synchronized (hotkeys.all()) { for (Hotkey h : hotkeys.all()) {
for (Hotkey h : hotkeys.all()) { if (!h.enabled || !h.isValid() || !h.action.supported()) continue;
if (!h.enabled || !h.isValid() || !h.action.supported()) continue; if (!h.combo.contains(key)) continue;
if (!h.combo.contains(key)) continue;
boolean satisfied = h.combo.isSatisfiedBy(pressed); boolean satisfied = h.combo.isSatisfiedBy(pressed);
if (satisfied && active.add(h)) { if (satisfied && active.add(h)) {
if (h.action.momentary() || h.trigger == Hotkey.Trigger.KEY_DOWN) { if (h.action.momentary() || h.trigger == Hotkey.Trigger.KEY_DOWN) {
fired.add(() -> handler.onHotkey(h, true)); fired.add(() -> handler.onHotkey(h, true));
} }
} else if (!satisfied && active.remove(h)) { } else if (!satisfied && active.remove(h)) {
if (h.action.momentary()) { if (h.action.momentary()) {
fired.add(() -> handler.onHotkey(h, false)); fired.add(() -> handler.onHotkey(h, false));
} else if (h.trigger == Hotkey.Trigger.KEY_UP) { } else if (h.trigger == Hotkey.Trigger.KEY_UP) {
fired.add(() -> handler.onHotkey(h, true)); fired.add(() -> handler.onHotkey(h, true));
}
} }
} }
} }

View File

@@ -22,9 +22,11 @@ public final class Hotkeys {
private final List<Hotkey> entries = Collections.synchronizedList(new ArrayList<>()); private final List<Hotkey> entries = Collections.synchronizedList(new ArrayList<>());
/** Live view; iterate under {@code synchronized (all())} when the engine may be running. */ /** The bindings as they are now; later changes do not show up in the returned list. */
public List<Hotkey> all() { public List<Hotkey> all() {
return entries; synchronized (entries) {
return List.copyOf(entries);
}
} }
public void add(Hotkey hotkey) { public void add(Hotkey hotkey) {

View File

@@ -70,10 +70,8 @@ final class HotkeyService {
/** The binding for an action with no argument, or {@code null} when unbound. */ /** The binding for an action with no argument, or {@code null} when unbound. */
Hotkey find(HotkeyAction action) { Hotkey find(HotkeyAction action) {
synchronized (hotkeys.all()) { for (Hotkey h : hotkeys.all()) {
for (Hotkey h : hotkeys.all()) { if (h.action == action) return h;
if (h.action == action) return h;
}
} }
return null; return null;
} }

View File

@@ -39,9 +39,7 @@ final class HotkeysPanel extends JPanel {
this.service = service; this.service = service;
setBorder(new ScaledEmptyBorder(8, 8, 8, 8)); setBorder(new ScaledEmptyBorder(8, 8, 8, 8));
synchronized (service.all()) { for (Hotkey h : service.all()) working.add(h.copy());
for (Hotkey h : service.all()) working.add(h.copy());
}
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setRowHeight(table.getRowHeight() + UIScale.scale(4)); table.setRowHeight(table.getRowHeight() + UIScale.scale(4));
@@ -113,9 +111,7 @@ final class HotkeysPanel extends JPanel {
/** Picks the stored bindings up again after something else changed them. */ /** Picks the stored bindings up again after something else changed them. */
void reload() { void reload() {
working.clear(); working.clear();
synchronized (service.all()) { for (Hotkey h : service.all()) working.add(h.copy());
for (Hotkey h : service.all()) working.add(h.copy());
}
model.fireTableDataChanged(); model.fireTableDataChanged();
} }

View File

@@ -325,10 +325,8 @@ final class VoiceActivationPanel extends FormPanel {
dlg.setVisible(true); dlg.setVisible(true);
if (!dlg.isConfirmed()) return; if (!dlg.isConfirmed()) return;
List<Hotkey> updated = new java.util.ArrayList<>(); List<Hotkey> updated = new java.util.ArrayList<>();
synchronized (hotkeys.all()) { for (Hotkey h : hotkeys.all()) {
for (Hotkey h : hotkeys.all()) { if (h != existing) updated.add(h.copy());
if (h != existing) updated.add(h.copy());
}
} }
updated.add(dlg.result()); updated.add(dlg.result());
hotkeys.replaceAll(updated); hotkeys.replaceAll(updated);