Keybinds: adjust layout, reject duplicate keybinds

This commit is contained in:
Lysec
2026-02-11 22:15:31 +01:00
parent c10f9b0f7e
commit 95313e1d24
20 changed files with 143 additions and 7 deletions
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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": "確認 / アクション",
+2
View File
@@ -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": "확인 / 동작",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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": "Подтвердить / Действие",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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": "Підтвердити / Дія",
+2
View File
@@ -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": "确认 / 操作",
+2
View File
@@ -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": "確認 / 動作",
+59
View File
@@ -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;
}
@@ -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
}
}
@@ -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();
+45 -7
View File
@@ -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