From 49778089eaf04f76f295cfdad5cf590c130ec950 Mon Sep 17 00:00:00 2001 From: lwouis Date: Mon, 25 May 2026 09:33:20 +0200 Subject: [PATCH] fix: rare crash when assigning shortcuts with arrow keys --- resources/l10n/Localizable.strings | 3 + resources/l10n/ar.lproj/Localizable.strings | 1 + resources/l10n/de.lproj/Localizable.strings | 1 + resources/l10n/en.lproj/Localizable.strings | 1 + resources/l10n/es.lproj/Localizable.strings | 1 + resources/l10n/fr.lproj/Localizable.strings | 1 + resources/l10n/he.lproj/Localizable.strings | 1 + resources/l10n/id.lproj/Localizable.strings | 1 + resources/l10n/it.lproj/Localizable.strings | 1 + resources/l10n/ja.lproj/Localizable.strings | 1 + resources/l10n/ko.lproj/Localizable.strings | 1 + resources/l10n/nl.lproj/Localizable.strings | 1 + resources/l10n/pl.lproj/Localizable.strings | 1 + .../l10n/pt-BR.lproj/Localizable.strings | 1 + resources/l10n/ru.lproj/Localizable.strings | 1 + resources/l10n/sv.lproj/Localizable.strings | 1 + resources/l10n/th.lproj/Localizable.strings | 1 + resources/l10n/tr.lproj/Localizable.strings | 1 + resources/l10n/vi.lproj/Localizable.strings | 1 + .../l10n/zh-CN.lproj/Localizable.strings | 1 + .../l10n/zh-HK.lproj/Localizable.strings | 1 + .../l10n/zh-TW.lproj/Localizable.strings | 1 + src/kit/CustomRecorderControl.swift | 68 +++++++++++++----- .../tabs/controls/ControlsTab.swift | 70 ++++++++++++++++--- 24 files changed, 132 insertions(+), 30 deletions(-) diff --git a/resources/l10n/Localizable.strings b/resources/l10n/Localizable.strings index 74b9bf76..a9ad3683 100644 --- a/resources/l10n/Localizable.strings +++ b/resources/l10n/Localizable.strings @@ -160,6 +160,9 @@ /* No comment provided by engineer. */ "Arrow keys" = "Arrow keys"; +/* No comment provided by engineer. */ +"Arrow keys already assigned to other actions:\n%@" = "Arrow keys already assigned to other actions:\n%@"; + /* No comment provided by engineer. */ "Ask whether to send crash reports" = "Ask whether to send crash reports"; diff --git a/resources/l10n/ar.lproj/Localizable.strings b/resources/l10n/ar.lproj/Localizable.strings index dd6a1ec0..34694a24 100644 --- a/resources/l10n/ar.lproj/Localizable.strings +++ b/resources/l10n/ar.lproj/Localizable.strings @@ -52,6 +52,7 @@ "Application Name - Window Title" = "اسم التطبيق - عنوان النافذة"; "Applies in other Appearances" = "يُطبَّق في المظاهر الأخرى"; "Arrow keys" = "مفاتيح الأسهم"; +"Arrow keys already assigned to other actions:\n%@" = "مفاتيح الأسهم معيّنة بالفعل لإجراءات أخرى:\n%@"; "Ask whether to send crash reports" = "السؤال عن إرسال تقارير الأعطال"; "Auto" = "تلقائي"; "Auto Size — larger previews with few windows, more at once with many" = "حجم تلقائي — معاينات أكبر مع عدد قليل من النوافذ، وأكثر مرة واحدة مع الكثير منها"; diff --git a/resources/l10n/de.lproj/Localizable.strings b/resources/l10n/de.lproj/Localizable.strings index 8ec6c373..82ea89c0 100644 --- a/resources/l10n/de.lproj/Localizable.strings +++ b/resources/l10n/de.lproj/Localizable.strings @@ -52,6 +52,7 @@ "Application Name - Window Title" = "Anwendungsname - Fenstertitel"; "Applies in other Appearances" = "Gilt in anderen Darstellungen"; "Arrow keys" = "Pfeiltasten"; +"Arrow keys already assigned to other actions:\n%@" = "Pfeiltasten sind bereits anderen Aktionen zugewiesen:\n%@"; "Ask whether to send crash reports" = "Fragen, ob Absturzberichte gesendet werden sollen"; "Auto" = "Automatisch"; "Auto Size — larger previews with few windows, more at once with many" = "Automatische Größe — größere Vorschauen bei wenigen Fenstern, mehr auf einmal bei vielen"; diff --git a/resources/l10n/en.lproj/Localizable.strings b/resources/l10n/en.lproj/Localizable.strings index 2a987247..737aa981 100644 --- a/resources/l10n/en.lproj/Localizable.strings +++ b/resources/l10n/en.lproj/Localizable.strings @@ -52,6 +52,7 @@ "Application Name - Window Title" = "Application Name - Window Title"; "Applies in other Appearances" = "Applies in other Appearances"; "Arrow keys" = "Arrow keys"; +"Arrow keys already assigned to other actions:\n%@" = "Arrow keys already assigned to other actions:\n%@"; "Ask whether to send crash reports" = "Ask whether to send crash reports"; "Auto" = "Auto"; "Auto Size — larger previews with few windows, more at once with many" = "Auto Size — larger previews with few windows, more at once with many"; diff --git a/resources/l10n/es.lproj/Localizable.strings b/resources/l10n/es.lproj/Localizable.strings index b7d7a62b..a8b858ef 100644 --- a/resources/l10n/es.lproj/Localizable.strings +++ b/resources/l10n/es.lproj/Localizable.strings @@ -52,6 +52,7 @@ "Application Name - Window Title" = "Nombre de la aplicación - Título de la ventana"; "Applies in other Appearances" = "Se aplica en otras Apariencias"; "Arrow keys" = "Teclas de flecha"; +"Arrow keys already assigned to other actions:\n%@" = "Teclas de flecha ya asignadas a otras acciones:\n%@"; "Ask whether to send crash reports" = "Preguntar si enviar los informes de problema"; "Auto" = "Automático"; "Auto Size — larger previews with few windows, more at once with many" = "Tamaño automático — previsualizaciones más grandes con pocas ventanas, más a la vez con muchas"; diff --git a/resources/l10n/fr.lproj/Localizable.strings b/resources/l10n/fr.lproj/Localizable.strings index ca5d9b27..5e5729a5 100644 --- a/resources/l10n/fr.lproj/Localizable.strings +++ b/resources/l10n/fr.lproj/Localizable.strings @@ -52,6 +52,7 @@ "Application Name - Window Title" = "Nom de l'application - Titre de la fenêtre"; "Applies in other Appearances" = "S'applique aux autres apparences"; "Arrow keys" = "Touches fléchées"; +"Arrow keys already assigned to other actions:\n%@" = "Touches fléchées déjà affectées à d'autres actions :\n%@"; "Ask whether to send crash reports" = "Demander avant d'envoyer un rapport de plantage"; "Auto" = "Auto"; "Auto Size — larger previews with few windows, more at once with many" = "Taille automatique — aperçus plus grands avec peu de fenêtres, plus à la fois quand il y en a beaucoup"; diff --git a/resources/l10n/he.lproj/Localizable.strings b/resources/l10n/he.lproj/Localizable.strings index 39066b9c..ccbd9881 100644 --- a/resources/l10n/he.lproj/Localizable.strings +++ b/resources/l10n/he.lproj/Localizable.strings @@ -52,6 +52,7 @@ "Application Name - Window Title" = "שם היישום - כותרת החלון"; "Applies in other Appearances" = "חל במראים אחרים"; "Arrow keys" = "מקשי חצים"; +"Arrow keys already assigned to other actions:\n%@" = "מקשי חצים כבר משויכים לפעולות אחרות:\n%@"; "Ask whether to send crash reports" = "שאל לפני שליחת דיווחי קריסה"; "Auto" = "אוטומטי"; "Auto Size — larger previews with few windows, more at once with many" = "גודל אוטומטי — תצוגות מקדימות גדולות יותר עם מעט חלונות, יותר בבת אחת עם רבים"; diff --git a/resources/l10n/id.lproj/Localizable.strings b/resources/l10n/id.lproj/Localizable.strings index d317b12b..d05df8c6 100644 --- a/resources/l10n/id.lproj/Localizable.strings +++ b/resources/l10n/id.lproj/Localizable.strings @@ -52,6 +52,7 @@ "Application Name - Window Title" = "Nama Aplikasi - Judul Jendela"; "Applies in other Appearances" = "Berlaku pada Tampilan lain"; "Arrow keys" = "Tombol panah"; +"Arrow keys already assigned to other actions:\n%@" = "Tombol panah sudah ditetapkan untuk tindakan lain:\n%@"; "Ask whether to send crash reports" = "Tanya apakah akan mengirim laporan kerusakan"; "Auto" = "Otomatis"; "Auto Size — larger previews with few windows, more at once with many" = "Ukuran Otomatis — pratinjau lebih besar saat jendela sedikit, lebih banyak sekaligus saat jendela banyak"; diff --git a/resources/l10n/it.lproj/Localizable.strings b/resources/l10n/it.lproj/Localizable.strings index 72be8346..fdff20c2 100644 --- a/resources/l10n/it.lproj/Localizable.strings +++ b/resources/l10n/it.lproj/Localizable.strings @@ -52,6 +52,7 @@ "Application Name - Window Title" = "Nome dell'applicazione - Titolo della finestra"; "Applies in other Appearances" = "Si applica negli altri Aspetti"; "Arrow keys" = "Tasti freccia"; +"Arrow keys already assigned to other actions:\n%@" = "Tasti freccia già assegnati ad altre azioni:\n%@"; "Ask whether to send crash reports" = "Chiedi se inviare i resoconti di arresti anomali"; "Auto" = "Automatico"; "Auto Size — larger previews with few windows, more at once with many" = "Dimensione automatica — anteprime più grandi con poche finestre, più contemporaneamente con molte"; diff --git a/resources/l10n/ja.lproj/Localizable.strings b/resources/l10n/ja.lproj/Localizable.strings index b73e0630..d8a606b5 100644 --- a/resources/l10n/ja.lproj/Localizable.strings +++ b/resources/l10n/ja.lproj/Localizable.strings @@ -52,6 +52,7 @@ "Application Name - Window Title" = "アプリケーション名 - ウインドウタイトル"; "Applies in other Appearances" = "他の外観にも適用されます"; "Arrow keys" = "矢印キー"; +"Arrow keys already assigned to other actions:\n%@" = "矢印キーはすでに他のアクションに割り当てられています:\n%@"; "Ask whether to send crash reports" = "クラッシュレポートの送信を毎回確認"; "Auto" = "自動"; "Auto Size — larger previews with few windows, more at once with many" = "自動サイズ — ウインドウが少ないと大きく、多いとまとめて表示"; diff --git a/resources/l10n/ko.lproj/Localizable.strings b/resources/l10n/ko.lproj/Localizable.strings index 0c27b9ef..d9205a38 100644 --- a/resources/l10n/ko.lproj/Localizable.strings +++ b/resources/l10n/ko.lproj/Localizable.strings @@ -52,6 +52,7 @@ "Application Name - Window Title" = "응용 프로그램 이름 - 윈도우 제목"; "Applies in other Appearances" = "다른 모양에도 적용됨"; "Arrow keys" = "화살표 키"; +"Arrow keys already assigned to other actions:\n%@" = "화살표 키가 이미 다른 동작에 할당되어 있습니다:\n%@"; "Ask whether to send crash reports" = "충돌 보고서 전송 여부 물어보기"; "Auto" = "자동"; "Auto Size — larger previews with few windows, more at once with many" = "자동 크기 — 윈도우가 적으면 미리보기가 크고, 많으면 한 번에 더 많이 표시"; diff --git a/resources/l10n/nl.lproj/Localizable.strings b/resources/l10n/nl.lproj/Localizable.strings index 2037b603..ac7ea4b6 100644 --- a/resources/l10n/nl.lproj/Localizable.strings +++ b/resources/l10n/nl.lproj/Localizable.strings @@ -52,6 +52,7 @@ "Application Name - Window Title" = "Naam van app - Venstertitel"; "Applies in other Appearances" = "Geldt in andere weergaven"; "Arrow keys" = "Pijltjestoetsen"; +"Arrow keys already assigned to other actions:\n%@" = "Pijltjestoetsen zijn al toegewezen aan andere acties:\n%@"; "Ask whether to send crash reports" = "Vragen of crashrapporten verstuurd moeten worden"; "Auto" = "Automatisch"; "Auto Size — larger previews with few windows, more at once with many" = "Automatische grootte — grotere voorvertoningen bij weinig vensters, meer tegelijk bij veel"; diff --git a/resources/l10n/pl.lproj/Localizable.strings b/resources/l10n/pl.lproj/Localizable.strings index 82615746..893dbadc 100644 --- a/resources/l10n/pl.lproj/Localizable.strings +++ b/resources/l10n/pl.lproj/Localizable.strings @@ -52,6 +52,7 @@ "Application Name - Window Title" = "Nazwa aplikacji - Tytuł okna"; "Applies in other Appearances" = "Stosuje się w innych Wyglądach"; "Arrow keys" = "Klawisze strzałek"; +"Arrow keys already assigned to other actions:\n%@" = "Klawisze strzałek są już przypisane do innych akcji:\n%@"; "Ask whether to send crash reports" = "Pytaj o wysyłanie raportów awarii"; "Auto" = "Automatycznie"; "Auto Size — larger previews with few windows, more at once with many" = "Automatyczny rozmiar — większe podglądy przy małej liczbie okien, więcej naraz przy dużej"; diff --git a/resources/l10n/pt-BR.lproj/Localizable.strings b/resources/l10n/pt-BR.lproj/Localizable.strings index 181dc36e..b0f93436 100644 --- a/resources/l10n/pt-BR.lproj/Localizable.strings +++ b/resources/l10n/pt-BR.lproj/Localizable.strings @@ -52,6 +52,7 @@ "Application Name - Window Title" = "Nome do Aplicativo - Título da Janela"; "Applies in other Appearances" = "Aplica-se a outras aparências"; "Arrow keys" = "Teclas de seta"; +"Arrow keys already assigned to other actions:\n%@" = "Teclas de seta já atribuídas a outras ações:\n%@"; "Ask whether to send crash reports" = "Perguntar se deve enviar relatórios de falha"; "Auto" = "Automático"; "Auto Size — larger previews with few windows, more at once with many" = "Tamanho automático — pré-visualizações maiores com poucas janelas, mais janelas de uma vez quando houver muitas"; diff --git a/resources/l10n/ru.lproj/Localizable.strings b/resources/l10n/ru.lproj/Localizable.strings index bb61d278..87246ebe 100644 --- a/resources/l10n/ru.lproj/Localizable.strings +++ b/resources/l10n/ru.lproj/Localizable.strings @@ -52,6 +52,7 @@ "Application Name - Window Title" = "Название приложения — Заголовок окна"; "Applies in other Appearances" = "Применяется в других оформлениях"; "Arrow keys" = "Клавиши со стрелками"; +"Arrow keys already assigned to other actions:\n%@" = "Клавиши со стрелками уже назначены другим действиям:\n%@"; "Ask whether to send crash reports" = "Спрашивать перед отправкой отчётов о сбоях"; "Auto" = "Авто"; "Auto Size — larger previews with few windows, more at once with many" = "Автоматический размер — крупнее при малом числе окон, компактнее при большом"; diff --git a/resources/l10n/sv.lproj/Localizable.strings b/resources/l10n/sv.lproj/Localizable.strings index bff33f74..c11fa9ac 100644 --- a/resources/l10n/sv.lproj/Localizable.strings +++ b/resources/l10n/sv.lproj/Localizable.strings @@ -52,6 +52,7 @@ "Application Name - Window Title" = "Programnamn - Fönstertitel"; "Applies in other Appearances" = "Gäller i andra Utseenden"; "Arrow keys" = "Piltangenter"; +"Arrow keys already assigned to other actions:\n%@" = "Piltangenter är redan tilldelade andra åtgärder:\n%@"; "Ask whether to send crash reports" = "Fråga om kraschrapporter ska skickas"; "Auto" = "Auto"; "Auto Size — larger previews with few windows, more at once with many" = "Automatisk storlek — större förhandsvisningar med få fönster, fler åt gången med många"; diff --git a/resources/l10n/th.lproj/Localizable.strings b/resources/l10n/th.lproj/Localizable.strings index 6e89473e..e2acce55 100644 --- a/resources/l10n/th.lproj/Localizable.strings +++ b/resources/l10n/th.lproj/Localizable.strings @@ -52,6 +52,7 @@ "Application Name - Window Title" = "ชื่อแอปพลิเคชัน - ชื่อหน้าต่าง"; "Applies in other Appearances" = "ใช้ในลักษณะอื่น"; "Arrow keys" = "ปุ่มลูกศร"; +"Arrow keys already assigned to other actions:\n%@" = "ปุ่มลูกศรถูกกำหนดให้กับการกระทำอื่นแล้ว:\n%@"; "Ask whether to send crash reports" = "ถามว่าจะส่งรายงานการขัดข้องหรือไม่"; "Auto" = "อัตโนมัติ"; "Auto Size — larger previews with few windows, more at once with many" = "ขนาดอัตโนมัติ — ภาพตัวอย่างใหญ่ขึ้นเมื่อมีหน้าต่างน้อย แสดงพร้อมกันมากขึ้นเมื่อมีหน้าต่างเยอะ"; diff --git a/resources/l10n/tr.lproj/Localizable.strings b/resources/l10n/tr.lproj/Localizable.strings index 1db67034..f03e9f4f 100644 --- a/resources/l10n/tr.lproj/Localizable.strings +++ b/resources/l10n/tr.lproj/Localizable.strings @@ -52,6 +52,7 @@ "Application Name - Window Title" = "Uygulama Adı - Pencere Başlığı"; "Applies in other Appearances" = "Diğer Görünümlerde uygulanır"; "Arrow keys" = "Ok tuşları"; +"Arrow keys already assigned to other actions:\n%@" = "Ok tuşları zaten başka eylemlere atanmış:\n%@"; "Ask whether to send crash reports" = "Çökme raporlarının gönderilip gönderilmeyeceğini sor"; "Auto" = "Otomatik"; "Auto Size — larger previews with few windows, more at once with many" = "Otomatik Boyut — az pencerede büyük önizlemeler, çok pencerede aynı anda daha fazlası"; diff --git a/resources/l10n/vi.lproj/Localizable.strings b/resources/l10n/vi.lproj/Localizable.strings index 0f27e885..396cda38 100644 --- a/resources/l10n/vi.lproj/Localizable.strings +++ b/resources/l10n/vi.lproj/Localizable.strings @@ -52,6 +52,7 @@ "Application Name - Window Title" = "Tên ứng dụng - Tiêu đề cửa sổ"; "Applies in other Appearances" = "Áp dụng trong các Giao diện khác"; "Arrow keys" = "Phím mũi tên"; +"Arrow keys already assigned to other actions:\n%@" = "Phím mũi tên đã được gán cho các hành động khác:\n%@"; "Ask whether to send crash reports" = "Hỏi trước khi gửi báo cáo sự cố"; "Auto" = "Tự động"; "Auto Size — larger previews with few windows, more at once with many" = "Kích cỡ tự động — bản xem trước lớn hơn khi có ít cửa sổ, hiển thị nhiều hơn cùng lúc khi có nhiều cửa sổ"; diff --git a/resources/l10n/zh-CN.lproj/Localizable.strings b/resources/l10n/zh-CN.lproj/Localizable.strings index 6c53982e..d42c8a4b 100644 --- a/resources/l10n/zh-CN.lproj/Localizable.strings +++ b/resources/l10n/zh-CN.lproj/Localizable.strings @@ -52,6 +52,7 @@ "Application Name - Window Title" = "应用名称 - 窗口标题"; "Applies in other Appearances" = "适用于其他外观"; "Arrow keys" = "方向键"; +"Arrow keys already assigned to other actions:\n%@" = "方向键已分配给其他操作:\n%@"; "Ask whether to send crash reports" = "询问是否发送崩溃报告"; "Auto" = "自动"; "Auto Size — larger previews with few windows, more at once with many" = "自动大小 — 窗口较少时预览更大,窗口较多时一次显示更多"; diff --git a/resources/l10n/zh-HK.lproj/Localizable.strings b/resources/l10n/zh-HK.lproj/Localizable.strings index 7ffd6ec4..705298e6 100644 --- a/resources/l10n/zh-HK.lproj/Localizable.strings +++ b/resources/l10n/zh-HK.lproj/Localizable.strings @@ -52,6 +52,7 @@ "Application Name - Window Title" = "應用程式名稱 - 視窗標題"; "Applies in other Appearances" = "套用至其他外觀"; "Arrow keys" = "方向鍵"; +"Arrow keys already assigned to other actions:\n%@" = "方向鍵已指派給其他動作:\n%@"; "Ask whether to send crash reports" = "詢問是否傳送當機報告"; "Auto" = "自動"; "Auto Size — larger previews with few windows, more at once with many" = "自動大小 — 視窗少時預覽較大,視窗多時一次顯示更多"; diff --git a/resources/l10n/zh-TW.lproj/Localizable.strings b/resources/l10n/zh-TW.lproj/Localizable.strings index 72c931a3..1ecccef6 100644 --- a/resources/l10n/zh-TW.lproj/Localizable.strings +++ b/resources/l10n/zh-TW.lproj/Localizable.strings @@ -52,6 +52,7 @@ "Application Name - Window Title" = "應用程式名稱 - 視窗標題"; "Applies in other Appearances" = "套用至其他外觀"; "Arrow keys" = "方向鍵"; +"Arrow keys already assigned to other actions:\n%@" = "方向鍵已指派給其他動作:\n%@"; "Ask whether to send crash reports" = "詢問是否傳送當機報告"; "Auto" = "自動"; "Auto Size — larger previews with few windows, more at once with many" = "自動調整大小 — 視窗少時預覽較大,視窗多時一次顯示更多"; diff --git a/src/kit/CustomRecorderControl.swift b/src/kit/CustomRecorderControl.swift index ab9ef87b..6e37d02f 100644 --- a/src/kit/CustomRecorderControl.swift +++ b/src/kit/CustomRecorderControl.swift @@ -41,15 +41,39 @@ class CustomRecorderControl: RecorderControl { set(allowedModifierFlags: CustomRecorderControl.allowedModifiers.subtracting(restrictedModifiers), requiredModifierFlags: [], allowsEmptyModifierFlags: true) } + private enum ShortcutConflict { + case arrow + case vim + case regular(control: CustomRecorderControl, label: String) + case unknown + + var label: String { + switch self { + case .arrow: return "Arrow keys" + case .vim: return "Vim keys" + case .regular(_, let label): return label + case .unknown: return "an unknown action" + } + } + + static func classify(_ id: String) -> ShortcutConflict { + if ["←", "→", "↑", "↓"].contains(id) { return .arrow } + if id.starts(with: "vimCycle") { return .vim } + if let existing = ControlsTab.shortcutControls[id] { + return .regular(control: existing.0, label: existing.1) + } + assertionFailure("conflict id '\(id)' missing from shortcutControls") + return .unknown + } + } + func alertIfSameShortcutAlreadyAssigned(_ candidateShortcut: Shortcut, _ shortcutAlreadyAssigned: String) { - let isArrowKeys = ["←", "→", "↑", "↓"].contains(shortcutAlreadyAssigned) - let isVimKeys = shortcutAlreadyAssigned.starts(with: "vimCycle") - let existingShortcut = ControlsTab.shortcutControls[shortcutAlreadyAssigned]! + let conflict = ShortcutConflict.classify(shortcutAlreadyAssigned) let alert = NSAlert() alert.alertStyle = .warning alert.messageText = NSLocalizedString("Conflicting shortcut", comment: "") alert.informativeText = String(format: NSLocalizedString("Shortcut already assigned to another action: %@", comment: ""), - (isArrowKeys ? "Arrow keys" : (isVimKeys ? "Vim keys" : existingShortcut.1)).replacingOccurrences(of: " ", with: "\u{00A0}")) + conflict.label.replacingOccurrences(of: " ", with: "\u{00A0}")) if !id.starts(with: "holdShortcut") { alert.addButton(withTitle: NSLocalizedString("Unassign existing shortcut and continue", comment: "")).setAccessibilityFocused(true) } @@ -59,22 +83,26 @@ class CustomRecorderControl: RecorderControl { cancelButton.setAccessibilityFocused(true) } let userChoice = alert.runModal() - if !id.starts(with: "holdShortcut") && userChoice == .alertFirstButtonReturn { - if isArrowKeys, let cb = ControlsTab.arrowKeysCheckbox { + guard !id.starts(with: "holdShortcut"), userChoice == .alertFirstButtonReturn else { return } + switch conflict { + case .arrow: + if let cb = ControlsTab.arrowKeysCheckbox { cb.state = .off ControlsTab.arrowKeysEnabledCallback(cb) LabelAndControl.controlWasChanged(cb, nil) - } else if isVimKeys, let cb = ControlsTab.vimKeysCheckbox { + } + case .vim: + if let cb = ControlsTab.vimKeysCheckbox { cb.state = .off ControlsTab.vimKeysEnabledCallback(cb) LabelAndControl.controlWasChanged(cb, nil) - } else { - updateShortcut(existingShortcut.0, nil, existingShortcut.0, shortcutAlreadyAssigned) - } - if let target = ControlsTab.shortcutControls[id]?.0 { - updateShortcut(target, candidateShortcut, self, id) } + case .regular(let existingControl, _): + updateShortcut(existingControl, nil, existingControl, shortcutAlreadyAssigned) + case .unknown: + return } + updateShortcut(self, candidateShortcut, self, id) } func updateShortcut(_ control: CustomRecorderControl, _ objectValue: Shortcut?, _ senderControl: NSControl, _ id: String) { @@ -84,22 +112,24 @@ class CustomRecorderControl: RecorderControl { } func alertIfShortcutReservedByMacos(_ candidateShortcut: Shortcut, _ shortcutReservedByMacos: String) { - let existingShortcutLabel = ControlsTab.shortcutControls[shortcutReservedByMacos]!.1 + let existing = ControlsTab.shortcutControls[shortcutReservedByMacos] + if existing == nil { + assertionFailure("reserved-by-macos id '\(shortcutReservedByMacos)' missing from shortcutControls") + } let alert = NSAlert() alert.alertStyle = .warning alert.messageText = NSLocalizedString("Conflicting shortcut", comment: "") - alert.informativeText = String(format: NSLocalizedString("macOS reserves ⌘⌥⎋, ⌘⌥⇧⎋, and ⌘⌥⇧⌃⎋ for Force Quit and they cannot be unbound. AltTab cannot use them.\n\nYour change would assign one of these to: %@.", comment: ""), existingShortcutLabel) + alert.informativeText = String(format: NSLocalizedString("macOS reserves ⌘⌥⎋, ⌘⌥⇧⎋, and ⌘⌥⇧⌃⎋ for Force Quit and they cannot be unbound. AltTab cannot use them.\n\nYour change would assign one of these to: %@.", comment: ""), existing?.1 ?? "an unknown action") alert.addButton(withTitle: NSLocalizedString("Unassign existing shortcut and continue", comment: "")) let cancelButton = alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "")) cancelButton.keyEquivalent = "\u{1b}" cancelButton.setAccessibilityFocused(true) let userChoice = alert.runModal() - if userChoice == .alertFirstButtonReturn { - guard id != shortcutReservedByMacos else { return } - let existingShortcut = ControlsTab.shortcutControls[shortcutReservedByMacos]! - updateShortcut(existingShortcut.0, nil, existingShortcut.0, shortcutReservedByMacos) - updateShortcut(ControlsTab.shortcutControls[id]!.0, candidateShortcut, self, id) + guard userChoice == .alertFirstButtonReturn, id != shortcutReservedByMacos else { return } + if let existing { + updateShortcut(existing.0, nil, existing.0, shortcutReservedByMacos) } + updateShortcut(self, candidateShortcut, self, id) } func save(_ candidateShortcut: Shortcut) { diff --git a/src/preferences/settings-window/tabs/controls/ControlsTab.swift b/src/preferences/settings-window/tabs/controls/ControlsTab.swift index 606d862b..b9889c65 100644 --- a/src/preferences/settings-window/tabs/controls/ControlsTab.swift +++ b/src/preferences/settings-window/tabs/controls/ControlsTab.swift @@ -51,6 +51,7 @@ class ControlsTab { "showAppsOrWindows", ] private static let arrowKeys = ["←", "→", "↑", "↓"] + private static let arrowKeyCodes: Set = [.leftArrow, .rightArrow, .upArrow, .downArrow] private static let vimKeyActions = [ "h": "vimCycleLeft", "l": "vimCycleRight", @@ -92,7 +93,7 @@ class ControlsTab { static func initializePreferencesDependentState() { applyActiveShortcutPreferences() staticManagedShortcutPreferences.forEach { applyShortcutPreference($0) } - applyArrowKeysPreference() + applyArrowKeysPreferenceWithoutDialogs() applyVimKeysPreferenceWithoutDialogs() } @@ -123,8 +124,8 @@ class ControlsTab { // An override key changed (e.g. user picked a different value, or remembered-restore wrote). refreshUnlinkButtons() AppearanceTab.refreshAllOverrideInfoLabels() - case "arrowKeysEnabled": - applyArrowKeysPreference() + case "arrowKeysEnabled" where arrowKeysCheckbox == nil: + applyArrowKeysPreferenceWithoutDialogs() case "vimKeysEnabled" where vimKeysCheckbox == nil: applyVimKeysPreferenceWithoutDialogs() default: @@ -1197,7 +1198,42 @@ class ControlsTab { } @objc static func arrowKeysEnabledCallback(_ sender: NSControl) { - applyArrowKeysPreference() + if (sender as! Switch).state == .on { + if isClearArrowKeysSuccessful() { + arrowKeys.forEach { addShortcut(.down, .local, Shortcut(keyEquivalent: $0)!, $0, nil) } + } else { + (sender as! Switch).state = .off + Preferences.set("arrowKeysEnabled", "false", false) + } + } else { + arrowKeys.forEach { removeShortcutIfExists($0) } + } + } + + private static func isClearArrowKeysSuccessful() -> Bool { + var conflicts = [String: String]() + shortcuts.forEach { + guard arrowKeyCodes.contains($1.shortcut.keyCode) else { return } + guard !arrowKeys.contains($1.id) else { return } + if let label = conflictLabel($1.id) { + conflicts[$1.id] = label + } + } + if !conflicts.isEmpty { + if SettingsWindow.shared == nil || !shouldClearConflictingShortcuts(conflicts.map { $0.value }, NSLocalizedString("Arrow keys already assigned to other actions:\n%@", comment: "")) { + return false + } + conflicts.forEach { + removeShortcutIfExists($0.key) + let existing = shortcutControls[$0.key] + if existing != nil { + existing!.0.objectValue = nil + shortcutChangedCallback(existing!.0) + LabelAndControl.controlWasChanged(existing!.0, $0.key) + } + } + } + return true } @objc static func vimKeysEnabledCallback(_ sender: NSControl) { @@ -1226,7 +1262,7 @@ class ControlsTab { } } if !conflicts.isEmpty { - if SettingsWindow.shared == nil || !shouldClearConflictingShortcuts(conflicts.map { $0.value }) { + if SettingsWindow.shared == nil || !shouldClearConflictingShortcuts(conflicts.map { $0.value }, NSLocalizedString("Vim keys already assigned to other actions:\n%@", comment: "")) { return false } conflicts.forEach { @@ -1255,12 +1291,12 @@ class ControlsTab { return nil } - private static func shouldClearConflictingShortcuts(_ conflicts: [String]) -> Bool { + private static func shouldClearConflictingShortcuts(_ conflicts: [String], _ messageFormat: String) -> Bool { let alert = NSAlert() alert.alertStyle = .warning alert.messageText = NSLocalizedString("Conflicting shortcut", comment: "") let informativeText = conflicts.map { "• " + $0 }.joined(separator: "\n") - alert.informativeText = String(format: NSLocalizedString("Vim keys already assigned to other actions:\n%@", comment: ""), informativeText.replacingOccurrences(of: " ", with: "\u{00A0}")) + alert.informativeText = String(format: messageFormat, informativeText.replacingOccurrences(of: " ", with: "\u{00A0}")) alert.addButton(withTitle: NSLocalizedString("Unassign existing shortcut and continue", comment: "")).setAccessibilityFocused(true) let cancelButton = alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "")) cancelButton.keyEquivalent = "\u{1b}" @@ -1323,11 +1359,23 @@ class ControlsTab { return Shortcut(code: baseShortcut.keyCode, modifierFlags: [holdShortcut.modifierFlags, baseShortcut.modifierFlags], characters: baseShortcut.characters, charactersIgnoringModifiers: baseShortcut.charactersIgnoringModifiers) } - private static func applyArrowKeysPreference() { - if Preferences.arrowKeysEnabled { - arrowKeys.forEach { addShortcut(.down, .local, Shortcut(keyEquivalent: $0)!, $0, nil) } - } else { + private static func applyArrowKeysPreferenceWithoutDialogs() { + guard Preferences.arrowKeysEnabled else { arrowKeys.forEach { removeShortcutIfExists($0) } + return + } + if hasArrowKeysConflictWithoutUi() { + arrowKeys.forEach { removeShortcutIfExists($0) } + Preferences.set("arrowKeysEnabled", "false", false) + return + } + arrowKeys.forEach { addShortcut(.down, .local, Shortcut(keyEquivalent: $0)!, $0, nil) } + } + + private static func hasArrowKeysConflictWithoutUi() -> Bool { + return shortcuts.values.contains { + guard arrowKeyCodes.contains($0.shortcut.keyCode) else { return false } + return !arrowKeys.contains($0.id) } }