From 95313e1d2429a7b2ffa27bc8486e36f96101b5ed Mon Sep 17 00:00:00 2001 From: Lysec Date: Wed, 11 Feb 2026 22:15:31 +0100 Subject: [PATCH] Keybinds: adjust layout, reject duplicate keybinds --- Assets/Translations/de.json | 2 + Assets/Translations/en.json | 2 + Assets/Translations/es.json | 2 + Assets/Translations/fr.json | 2 + Assets/Translations/hu.json | 2 + Assets/Translations/ja.json | 2 + Assets/Translations/ko-KR.json | 2 + Assets/Translations/nl.json | 2 + Assets/Translations/pl.json | 2 + Assets/Translations/pt.json | 2 + Assets/Translations/ru.json | 2 + Assets/Translations/sv.json | 2 + Assets/Translations/tr.json | 2 + Assets/Translations/uk-UA.json | 2 + Assets/Translations/zh-CN.json | 2 + Assets/Translations/zh-TW.json | 2 + Helpers/Keybinds.js | 59 +++++++++++++++++++ .../Settings/Tabs/General/KeybindsSubTab.qml | 6 ++ .../SessionMenuEntrySettingsDialog.qml | 1 + Widgets/NKeybindRecorder.qml | 52 +++++++++++++--- 20 files changed, 143 insertions(+), 7 deletions(-) diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index 2719d36af..5f1026b72 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -1030,6 +1030,8 @@ "fonts-monospace-search-placeholder": "Monospace-Schriftarten suchen...", "fonts-reset-scaling": "Skalierung zurücksetzen", "fonts-title": "Schriftarten", + "keybinds-conflict-description": "Die Tastenkombination ist bereits {action} zugewiesen.", + "keybinds-conflict-title": "Tastenbelegungskonflikt", "keybinds-description": "Globale Navigationstasten für Panels und Launcher konfigurieren.", "keybinds-down": "Nach unten verschieben", "keybinds-enter": "Bestätigen / Aktion", diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 2b35a50a8..f288f6c7e 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -1030,6 +1030,8 @@ "fonts-monospace-search-placeholder": "Search monospace font...", "fonts-reset-scaling": "Reset scaling", "fonts-title": "Fonts", + "keybinds-conflict-description": "The key combination is already assigned to {action}.", + "keybinds-conflict-title": "Keybind Conflict", "keybinds-description": "Configure global navigation keys for panels and launcher.", "keybinds-down": "Move Down", "keybinds-enter": "Confirm / Action", diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index 61a25fb8b..0fb360e8f 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -1030,6 +1030,8 @@ "fonts-monospace-search-placeholder": "Buscar fuentes monoespaciadas...", "fonts-reset-scaling": "Restablecer la escala", "fonts-title": "Fuentes", + "keybinds-conflict-description": "La combinación de teclas ya está asignada a {action}.", + "keybinds-conflict-title": "Conflicto de Atajos de Teclado", "keybinds-description": "Configura las teclas de navegación globales para paneles y el lanzador.", "keybinds-down": "Mover hacia abajo", "keybinds-enter": "Confirmar / Acción", diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index eada4b76e..d7ad9ce8b 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -1030,6 +1030,8 @@ "fonts-monospace-search-placeholder": "Rechercher des polices à chasse fixe...", "fonts-reset-scaling": "Réinitialiser l'échelle", "fonts-title": "Polices", + "keybinds-conflict-description": "La combinaison de touches est déjà attribuée à {action}.", + "keybinds-conflict-title": "Conflit de Raccourcis Clavier", "keybinds-description": "Configurez les touches de navigation globales pour les panneaux et le lanceur.", "keybinds-down": "Déplacer vers le bas", "keybinds-enter": "Confirmer / Action", diff --git a/Assets/Translations/hu.json b/Assets/Translations/hu.json index 0c608ccd1..06bc53c8c 100644 --- a/Assets/Translations/hu.json +++ b/Assets/Translations/hu.json @@ -1030,6 +1030,8 @@ "fonts-monospace-search-placeholder": "Monospace betűtípus keresése...", "fonts-reset-scaling": "Méretezés visszaállítása", "fonts-title": "Betűtípusok", + "keybinds-conflict-description": "A billentyűkombináció már hozzá van rendelve ehhez: {action}.", + "keybinds-conflict-title": "Billentyűkombináció Ütközés", "keybinds-description": "Globális navigációs billentyűk beállítása a panelekhez és az indítóhoz.", "keybinds-down": "Lejjebb mozgatás", "keybinds-enter": "Megerősítés / Művelet", diff --git a/Assets/Translations/ja.json b/Assets/Translations/ja.json index c57a5275b..cc59d99a7 100644 --- a/Assets/Translations/ja.json +++ b/Assets/Translations/ja.json @@ -1030,6 +1030,8 @@ "fonts-monospace-search-placeholder": "等幅フォントを検索...", "fonts-reset-scaling": "サイズ設定をリセット", "fonts-title": "フォント", + "keybinds-conflict-description": "このキーの組み合わせはすでに{action}に割り当てられています。", + "keybinds-conflict-title": "キーバインドの競合", "keybinds-description": "パネルとランチャーのグローバルナビゲーションキーを設定します。", "keybinds-down": "下に移動", "keybinds-enter": "確認 / アクション", diff --git a/Assets/Translations/ko-KR.json b/Assets/Translations/ko-KR.json index 86fb7f874..2997fcf04 100644 --- a/Assets/Translations/ko-KR.json +++ b/Assets/Translations/ko-KR.json @@ -1030,6 +1030,8 @@ "fonts-monospace-search-placeholder": "고정폭 글꼴 검색...", "fonts-reset-scaling": "크기 초기화", "fonts-title": "글꼴", + "keybinds-conflict-description": "이 키 조합은 이미 {action}에 할당되어 있습니다.", + "keybinds-conflict-title": "키 바인딩 충돌", "keybinds-description": "패널 및 런처에 대한 전역 탐색 키를 구성합니다.", "keybinds-down": "아래로 이동", "keybinds-enter": "확인 / 동작", diff --git a/Assets/Translations/nl.json b/Assets/Translations/nl.json index a4968ee4b..3586122ac 100644 --- a/Assets/Translations/nl.json +++ b/Assets/Translations/nl.json @@ -1030,6 +1030,8 @@ "fonts-monospace-search-placeholder": "Monospace-lettertype zoeken...", "fonts-reset-scaling": "Schaling resetten", "fonts-title": "Lettertypes", + "keybinds-conflict-description": "De toetscombinatie is al toegewezen aan {action}.", + "keybinds-conflict-title": "Toetsencombinatieconflict", "keybinds-description": "Globale navigatietoetsen voor panelen en de starter configureren.", "keybinds-down": "Naar beneden verplaatsen", "keybinds-enter": "Bevestigen / Actie", diff --git a/Assets/Translations/pl.json b/Assets/Translations/pl.json index a7630af93..5f3845115 100644 --- a/Assets/Translations/pl.json +++ b/Assets/Translations/pl.json @@ -1030,6 +1030,8 @@ "fonts-monospace-search-placeholder": "Szukaj czcionki monospace...", "fonts-reset-scaling": "Resetuj skalowanie", "fonts-title": "Czcionki", + "keybinds-conflict-description": "Kombinacja klawiszy jest już przypisana do {action}.", + "keybinds-conflict-title": "Konflikt Skrótów Klawiszowych", "keybinds-description": "Skonfiguruj globalne klawisze nawigacyjne dla paneli i launchera.", "keybinds-down": "Przenieś w dół", "keybinds-enter": "Potwierdź / Akcja", diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index 937aa85e2..88e070044 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -1030,6 +1030,8 @@ "fonts-monospace-search-placeholder": "Pesquisar fontes monoespaçadas...", "fonts-reset-scaling": "Redefinir escala", "fonts-title": "Fontes", + "keybinds-conflict-description": "A combinação de teclas já está atribuída a {action}.", + "keybinds-conflict-title": "Conflito de Atalhos de Teclado", "keybinds-description": "Configure as teclas de navegação globais para painéis e o lançador.", "keybinds-down": "Mover para baixo", "keybinds-enter": "Confirmar / Ação", diff --git a/Assets/Translations/ru.json b/Assets/Translations/ru.json index f6cba4bbe..307c19507 100644 --- a/Assets/Translations/ru.json +++ b/Assets/Translations/ru.json @@ -1030,6 +1030,8 @@ "fonts-monospace-search-placeholder": "Поиск моноширинного шрифта...", "fonts-reset-scaling": "Сбросить масштабирование", "fonts-title": "Шрифты", + "keybinds-conflict-description": "Комбинация клавиш уже назначена для {action}.", + "keybinds-conflict-title": "Конфликт Горячих Клавиш", "keybinds-description": "Настройте глобальные клавиши навигации для панелей и лаунчера.", "keybinds-down": "Переместить вниз", "keybinds-enter": "Подтвердить / Действие", diff --git a/Assets/Translations/sv.json b/Assets/Translations/sv.json index 9c526c4f5..1b27a1b77 100644 --- a/Assets/Translations/sv.json +++ b/Assets/Translations/sv.json @@ -1030,6 +1030,8 @@ "fonts-monospace-search-placeholder": "Sök teckensnitt med fast bredd...", "fonts-reset-scaling": "Återställ skalning", "fonts-title": "Teckensnitt", + "keybinds-conflict-description": "Tangentkombinationen är redan tilldelad {action}.", + "keybinds-conflict-title": "Tangentbindningskonflikt", "keybinds-description": "Konfigurera globala navigeringsknappar för paneler och startprogram.", "keybinds-down": "Flytta nedåt", "keybinds-enter": "Bekräfta / Åtgärd", diff --git a/Assets/Translations/tr.json b/Assets/Translations/tr.json index 2ee41078d..b2759b5a6 100644 --- a/Assets/Translations/tr.json +++ b/Assets/Translations/tr.json @@ -1030,6 +1030,8 @@ "fonts-monospace-search-placeholder": "Eş aralıklı yazı tipi ara...", "fonts-reset-scaling": "Ölçeklemeyi sıfırla", "fonts-title": "Yazı tipleri", + "keybinds-conflict-description": "Tuş kombinasyonu zaten {action} için atanmış.", + "keybinds-conflict-title": "Tuş Ataması Çakışması", "keybinds-description": "Paneller ve başlatıcı için genel navigasyon tuşlarını yapılandırın.", "keybinds-down": "Aşağı Taşı", "keybinds-enter": "Onayla / Eylem", diff --git a/Assets/Translations/uk-UA.json b/Assets/Translations/uk-UA.json index ac9a3d623..5ef123cde 100644 --- a/Assets/Translations/uk-UA.json +++ b/Assets/Translations/uk-UA.json @@ -1030,6 +1030,8 @@ "fonts-monospace-search-placeholder": "Пошук моноширинного шрифту...", "fonts-reset-scaling": "Скинути масштаб", "fonts-title": "Шрифти", + "keybinds-conflict-description": "Комбінація клавіш вже призначена для {action}.", + "keybinds-conflict-title": "Конфлікт Гарячих Клавіш", "keybinds-description": "Налаштуйте глобальні клавіші навігації для панелей та запускача.", "keybinds-down": "Перемістити вниз", "keybinds-enter": "Підтвердити / Дія", diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index 8aa5d9a48..584be9c72 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -1030,6 +1030,8 @@ "fonts-monospace-search-placeholder": "搜索等宽字体...", "fonts-reset-scaling": "恢复默认缩放", "fonts-title": "字体", + "keybinds-conflict-description": "此按键组合已分配给 {action}。", + "keybinds-conflict-title": "按键绑定冲突", "keybinds-description": "配置面板和启动器的全局导航键。", "keybinds-down": "下移", "keybinds-enter": "确认 / 操作", diff --git a/Assets/Translations/zh-TW.json b/Assets/Translations/zh-TW.json index 12760992b..ac065a44d 100644 --- a/Assets/Translations/zh-TW.json +++ b/Assets/Translations/zh-TW.json @@ -1030,6 +1030,8 @@ "fonts-monospace-search-placeholder": "搜尋等寬字型...", "fonts-reset-scaling": "重設文字大小", "fonts-title": "字型", + "keybinds-conflict-description": "此按鍵組合已分配給 {action}。", + "keybinds-conflict-title": "按鍵綁定衝突", "keybinds-description": "配置面板和啟動器的全域導航鍵。", "keybinds-down": "下移", "keybinds-enter": "確認 / 動作", diff --git a/Helpers/Keybinds.js b/Helpers/Keybinds.js index e21cb0fd2..371795e9f 100644 --- a/Helpers/Keybinds.js +++ b/Helpers/Keybinds.js @@ -109,3 +109,62 @@ function checkKey(event, settingName, settings) { } return false; } + +/** + * Check if a keybind string conflicts with any other existing keybinds. + * @param {string} keyStr - The keybind string to check (e.g., "Ctrl+A"). + * @param {string} currentPath - The settings path of the keybind being edited (to skip checking itself). + * @param {object} data - The settings data object (from Settings.data). + * @returns {string|null} - The name of the conflicting action, or null if no conflict. + */ +function getKeybindConflict(keyStr, currentPath, data) { + if (!keyStr || !data) return null; + + const searchKey = String(keyStr).trim().toLowerCase(); + + // 1. Check navigation keybinds + const navKeybinds = data.general ? data.general.keybinds : null; + const navMap = { + "keyUp": "Navigation: Up", + "keyDown": "Navigation: Down", + "keyLeft": "Navigation: Left", + "keyRight": "Navigation: Right", + "keyEnter": "Navigation: Enter", + "keyEscape": "Navigation: Escape" + }; + + if (navKeybinds) { + for (const prop in navMap) { + const fullPath = "general.keybinds." + prop; + if (fullPath === currentPath) continue; + + const boundKeys = navKeybinds[prop]; + if (boundKeys && boundKeys.length !== undefined) { + for (let i = 0; i < boundKeys.length; i++) { + if (String(boundKeys[i]).trim().toLowerCase() === searchKey) { + return navMap[prop]; + } + } + } + } + } + + // 2. Check session menu power options + const sessionMenu = data.sessionMenu; + if (sessionMenu && sessionMenu.powerOptions) { + const powerOptions = sessionMenu.powerOptions; + for (let i = 0; i < powerOptions.length; i++) { + const entry = powerOptions[i]; + const fullPath = "sessionMenu.powerOptions[" + i + "].keybind"; + if (fullPath === currentPath) continue; + + if (entry.keybind && String(entry.keybind).trim().toLowerCase() === searchKey) { + // Capitalize action name + const actionName = entry.action ? entry.action.charAt(0).toUpperCase() + entry.action.slice(1) : "Unknown"; + return "Session Menu: " + actionName; + } + } + } + + return null; +} diff --git a/Modules/Panels/Settings/Tabs/General/KeybindsSubTab.qml b/Modules/Panels/Settings/Tabs/General/KeybindsSubTab.qml index 05cb92e82..3364e103c 100644 --- a/Modules/Panels/Settings/Tabs/General/KeybindsSubTab.qml +++ b/Modules/Panels/Settings/Tabs/General/KeybindsSubTab.qml @@ -21,6 +21,7 @@ ColumnLayout { label: I18n.tr("panels.general.keybinds-up") currentKeybinds: Settings.data.general.keybinds.keyUp defaultKeybind: "Up" + settingsPath: "general.keybinds.keyUp" onKeybindsChanged: newKeybinds => Settings.data.general.keybinds.keyUp = newKeybinds } @@ -29,6 +30,7 @@ ColumnLayout { label: I18n.tr("panels.general.keybinds-down") currentKeybinds: Settings.data.general.keybinds.keyDown defaultKeybind: "Down" + settingsPath: "general.keybinds.keyDown" onKeybindsChanged: newKeybinds => Settings.data.general.keybinds.keyDown = newKeybinds } @@ -37,6 +39,7 @@ ColumnLayout { label: I18n.tr("panels.general.keybinds-left") currentKeybinds: Settings.data.general.keybinds.keyLeft defaultKeybind: "Left" + settingsPath: "general.keybinds.keyLeft" onKeybindsChanged: newKeybinds => Settings.data.general.keybinds.keyLeft = newKeybinds } @@ -45,6 +48,7 @@ ColumnLayout { label: I18n.tr("panels.general.keybinds-right") currentKeybinds: Settings.data.general.keybinds.keyRight defaultKeybind: "Right" + settingsPath: "general.keybinds.keyRight" onKeybindsChanged: newKeybinds => Settings.data.general.keybinds.keyRight = newKeybinds } @@ -53,6 +57,7 @@ ColumnLayout { label: I18n.tr("panels.general.keybinds-enter") currentKeybinds: Settings.data.general.keybinds.keyEnter defaultKeybind: "Return" + settingsPath: "general.keybinds.keyEnter" onKeybindsChanged: newKeybinds => Settings.data.general.keybinds.keyEnter = newKeybinds } @@ -61,6 +66,7 @@ ColumnLayout { label: I18n.tr("panels.general.keybinds-escape") currentKeybinds: Settings.data.general.keybinds.keyEscape defaultKeybind: "Esc" + settingsPath: "general.keybinds.keyEscape" onKeybindsChanged: newKeybinds => Settings.data.general.keybinds.keyEscape = newKeybinds } } diff --git a/Modules/Panels/Settings/Tabs/SessionMenu/SessionMenuEntrySettingsDialog.qml b/Modules/Panels/Settings/Tabs/SessionMenu/SessionMenuEntrySettingsDialog.qml index d1bafcafd..bbe155f8e 100644 --- a/Modules/Panels/Settings/Tabs/SessionMenu/SessionMenuEntrySettingsDialog.qml +++ b/Modules/Panels/Settings/Tabs/SessionMenu/SessionMenuEntrySettingsDialog.qml @@ -163,6 +163,7 @@ Popup { allowEmpty: true maxKeybinds: 1 currentKeybinds: keybindInputText ? [keybindInputText] : [] + settingsPath: "sessionMenu.powerOptions[" + root.entryIndex + "].keybind" onKeybindsChanged: newKeybinds => { keybindInputText = newKeybinds.length > 0 ? newKeybinds[0] : ""; root.save(); diff --git a/Widgets/NKeybindRecorder.qml b/Widgets/NKeybindRecorder.qml index 4810868a9..8054a52f9 100644 --- a/Widgets/NKeybindRecorder.qml +++ b/Widgets/NKeybindRecorder.qml @@ -4,6 +4,7 @@ import QtQuick.Layouts import qs.Commons import qs.Services.UI import qs.Widgets +import "../Helpers/Keybinds.js" as Keybinds Item { id: root @@ -15,6 +16,7 @@ Item { property bool allowEmpty: false property color labelColor: Color.mOnSurface property color descriptionColor: Color.mOnSurfaceVariant + property string settingsPath: "" property int maxKeybinds: 2 signal keybindsChanged(var newKeybinds) @@ -23,12 +25,39 @@ Item { // -1 = not recording, >= 0 = re-recording at index, -2 = adding new property int recordingIndex: -1 + property bool hasConflict: false - onRecordingIndexChanged: PanelService.isKeybindRecording = recordingIndex !== -1 + onRecordingIndexChanged: { + PanelService.isKeybindRecording = recordingIndex !== -1; + if (recordingIndex !== -1) { + hasConflict = false; + } + } readonly property real _pillHeight: Style.baseWidgetSize * 1.1 * Style.uiScaleRatio function _applyKeybind(keyStr) { + if (!keyStr) return; + + // 1. Internal duplicate check (same action) + for (let i = 0; i < root.currentKeybinds.length; i++) { + if (i !== root.recordingIndex && String(root.currentKeybinds[i]).toLowerCase() === keyStr.toLowerCase()) { + hasConflict = true; + ToastService.showWarning(I18n.tr("panels.general.keybinds-conflict-title"), I18n.tr("panels.general.keybinds-conflict-description", { "action": root.label || "This action" })); + conflictTimer.restart(); + return; + } + } + + // 2. External conflict check (other actions) + const conflict = Keybinds.getKeybindConflict(keyStr, root.settingsPath, Settings.data); + if (conflict) { + hasConflict = true; + ToastService.showWarning(I18n.tr("panels.general.keybinds-conflict-title"), I18n.tr("panels.general.keybinds-conflict-description", { "action": conflict })); + conflictTimer.restart(); + return; + } + var newKeybinds = Array.from(root.currentKeybinds); if (recordingIndex >= 0) { newKeybinds[recordingIndex] = keyStr; @@ -39,6 +68,15 @@ Item { root.keybindsChanged(newKeybinds); } + Timer { + id: conflictTimer + interval: 2000 + onTriggered: { + hasConflict = false; + recordingIndex = -1; + } + } + RowLayout { id: contentLayout width: parent.width @@ -87,8 +125,8 @@ Item { id: slotBg anchors.fill: parent radius: Style.iRadiusS - color: slotArea.isRecordingThis ? Color.mSecondary : (slotArea.containsMouse ? Qt.alpha(Color.mSecondary, 0.15) : Color.mSurface) - border.color: slotArea.isRecordingThis ? Color.mPrimary : (slotArea.containsMouse ? Color.mSecondary : Color.mOutline) + color: root.hasConflict && slotArea.isRecordingThis ? Color.mError : (slotArea.isRecordingThis ? Color.mSecondary : (slotArea.containsMouse ? Qt.alpha(Color.mSecondary, 0.15) : Color.mSurface)) + border.color: root.hasConflict && slotArea.isRecordingThis ? Color.mError : (slotArea.isRecordingThis ? Color.mPrimary : (slotArea.containsMouse ? Color.mSecondary : Color.mOutline)) border.width: Style.borderS Behavior on color { @@ -109,10 +147,10 @@ Item { spacing: Style.marginXS NIcon { - icon: slotArea.isRecordingThis ? "circle-dot" : "keyboard" + icon: root.hasConflict && slotArea.isRecordingThis ? "alert-circle" : (slotArea.isRecordingThis ? "circle-dot" : "keyboard") color: slotArea.isRecordingThis ? Color.mOnSecondary : (slotArea.isOccupied ? Color.mOnSurfaceVariant : Qt.alpha(Color.mOnSurfaceVariant, 0.4)) opacity: 0.8 - visible: !slotArea.isRecordingThis + visible: !slotArea.isRecordingThis || root.hasConflict } NText { @@ -129,7 +167,7 @@ Item { Item { Layout.preferredWidth: Math.round(root._pillHeight * 0.7) Layout.fillHeight: true - visible: slotArea.isOccupied + visible: slotArea.isOccupied && root.recordingIndex === -1 NIconButton { anchors.centerIn: parent @@ -162,7 +200,7 @@ Item { focus: true Keys.onPressed: event => { - if (root.recordingIndex === -1) + if (root.recordingIndex === -1 || root.hasConflict) return; // Handle Escape specifically to ensure it doesn't close the panel