diff --git a/core/internal/keybinds/providers/niri.go b/core/internal/keybinds/providers/niri.go index fe26e050..ce23e02c 100644 --- a/core/internal/keybinds/providers/niri.go +++ b/core/internal/keybinds/providers/niri.go @@ -5,6 +5,7 @@ import ( "os" "os/exec" "path/filepath" + "sort" "strings" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" @@ -53,14 +54,29 @@ func (n *NiriProvider) GetCheatSheet() (*keybinds.CheatSheet, error) { n.parsed = true categorizedBinds := make(map[string][]keybinds.Keybind) - n.convertSection(result.Section, "", categorizedBinds) + n.convertSection(result.Section, "", categorizedBinds, result.ConflictingConfigs) - return &keybinds.CheatSheet{ + sheet := &keybinds.CheatSheet{ Title: "Niri Keybinds", Provider: n.Name(), Binds: categorizedBinds, DMSBindsIncluded: result.DMSBindsIncluded, - }, nil + } + + if result.DMSStatus != nil { + sheet.DMSStatus = &keybinds.DMSBindsStatus{ + Exists: result.DMSStatus.Exists, + Included: result.DMSStatus.Included, + IncludePosition: result.DMSStatus.IncludePosition, + TotalIncludes: result.DMSStatus.TotalIncludes, + BindsAfterDMS: result.DMSStatus.BindsAfterDMS, + Effective: result.DMSStatus.Effective, + OverriddenBy: result.DMSStatus.OverriddenBy, + StatusMessage: result.DMSStatus.StatusMessage, + } + } + + return sheet, nil } func (n *NiriProvider) HasDMSBindsIncluded() bool { @@ -78,7 +94,7 @@ func (n *NiriProvider) HasDMSBindsIncluded() bool { return n.dmsBindsIncluded } -func (n *NiriProvider) convertSection(section *NiriSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind) { +func (n *NiriProvider) convertSection(section *NiriSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind, conflicts map[string]*NiriKeyBinding) { currentSubcat := subcategory if section.Name != "" { currentSubcat = section.Name @@ -86,12 +102,12 @@ func (n *NiriProvider) convertSection(section *NiriSection, subcategory string, for _, kb := range section.Keybinds { category := n.categorizeByAction(kb.Action) - bind := n.convertKeybind(&kb, currentSubcat) + bind := n.convertKeybind(&kb, currentSubcat, conflicts) categorizedBinds[category] = append(categorizedBinds[category], bind) } for _, child := range section.Children { - n.convertSection(&child, currentSubcat, categorizedBinds) + n.convertSection(&child, currentSubcat, categorizedBinds, conflicts) } } @@ -128,21 +144,35 @@ func (n *NiriProvider) categorizeByAction(action string) string { } } -func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string) keybinds.Keybind { +func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string, conflicts map[string]*NiriKeyBinding) keybinds.Keybind { rawAction := n.formatRawAction(kb.Action, kb.Args) + keyStr := n.formatKey(kb) source := "config" if strings.Contains(kb.Source, "dms/binds.kdl") { source = "dms" } - return keybinds.Keybind{ - Key: n.formatKey(kb), + bind := keybinds.Keybind{ + Key: keyStr, Description: kb.Description, Action: rawAction, Subcategory: subcategory, Source: source, } + + if source == "dms" && conflicts != nil { + if conflictKb, ok := conflicts[keyStr]; ok { + bind.Conflict = &keybinds.Keybind{ + Key: keyStr, + Description: conflictKb.Description, + Action: n.formatRawAction(conflictKb.Action, conflictKb.Args), + Source: "config", + } + } + } + + return bind } func (n *NiriProvider) formatRawAction(action string, args []string) string { @@ -386,6 +416,29 @@ func (n *NiriProvider) writeOverrideBinds(binds map[string]*overrideBind) error return os.WriteFile(overridePath, []byte(content), 0644) } +func (n *NiriProvider) getBindSortPriority(action string) int { + switch { + case strings.HasPrefix(action, "spawn") && strings.Contains(action, "dms"): + return 0 + case strings.Contains(action, "workspace"): + return 1 + case strings.Contains(action, "window") || strings.Contains(action, "column") || + strings.Contains(action, "focus") || strings.Contains(action, "move") || + strings.Contains(action, "swap") || strings.Contains(action, "resize"): + return 2 + case strings.HasPrefix(action, "focus-monitor") || strings.Contains(action, "monitor"): + return 3 + case strings.Contains(action, "screenshot"): + return 4 + case action == "quit" || action == "power-off-monitors" || strings.Contains(action, "dpms"): + return 5 + case strings.HasPrefix(action, "spawn"): + return 6 + default: + return 7 + } +} + func (n *NiriProvider) generateBindsContent(binds map[string]*overrideBind) string { if len(binds) == 0 { return "binds {}\n" @@ -401,6 +454,18 @@ func (n *NiriProvider) generateBindsContent(binds map[string]*overrideBind) stri } } + sort.Slice(regularBinds, func(i, j int) bool { + pi, pj := n.getBindSortPriority(regularBinds[i].Action), n.getBindSortPriority(regularBinds[j].Action) + if pi != pj { + return pi < pj + } + return regularBinds[i].Key < regularBinds[j].Key + }) + + sort.Slice(recentWindowsBinds, func(i, j int) bool { + return recentWindowsBinds[i].Key < recentWindowsBinds[j].Key + }) + var sb strings.Builder sb.WriteString("binds {\n") diff --git a/core/internal/keybinds/providers/niri_parser.go b/core/internal/keybinds/providers/niri_parser.go index cf6c034c..988f5c17 100644 --- a/core/internal/keybinds/providers/niri_parser.go +++ b/core/internal/keybinds/providers/niri_parser.go @@ -26,35 +26,78 @@ type NiriSection struct { } type NiriParser struct { - configDir string - processedFiles map[string]bool - bindMap map[string]*NiriKeyBinding - bindOrder []string - currentSource string - dmsBindsIncluded bool + configDir string + processedFiles map[string]bool + bindMap map[string]*NiriKeyBinding + bindOrder []string + currentSource string + dmsBindsIncluded bool + dmsBindsExists bool + includeCount int + dmsIncludePos int + bindsBeforeDMS int + bindsAfterDMS int + dmsBindKeys map[string]bool + configBindKeys map[string]bool + dmsProcessed bool + dmsBindMap map[string]*NiriKeyBinding + conflictingConfigs map[string]*NiriKeyBinding } func NewNiriParser(configDir string) *NiriParser { return &NiriParser{ - configDir: configDir, - processedFiles: make(map[string]bool), - bindMap: make(map[string]*NiriKeyBinding), - bindOrder: []string{}, - currentSource: "", + configDir: configDir, + processedFiles: make(map[string]bool), + bindMap: make(map[string]*NiriKeyBinding), + bindOrder: []string{}, + currentSource: "", + dmsIncludePos: -1, + dmsBindKeys: make(map[string]bool), + configBindKeys: make(map[string]bool), + dmsBindMap: make(map[string]*NiriKeyBinding), + conflictingConfigs: make(map[string]*NiriKeyBinding), } } func (p *NiriParser) Parse() (*NiriSection, error) { + dmsBindsPath := filepath.Join(p.configDir, "dms", "binds.kdl") + if _, err := os.Stat(dmsBindsPath); err == nil { + p.dmsBindsExists = true + } + configPath := filepath.Join(p.configDir, "config.kdl") section, err := p.parseFile(configPath, "") if err != nil { return nil, err } + if p.dmsBindsExists && !p.dmsProcessed { + p.parseDMSBindsDirectly(dmsBindsPath, section) + } + section.Keybinds = p.finalizeBinds() return section, nil } +func (p *NiriParser) parseDMSBindsDirectly(dmsBindsPath string, section *NiriSection) { + data, err := os.ReadFile(dmsBindsPath) + if err != nil { + return + } + + doc, err := kdl.Parse(strings.NewReader(string(data))) + if err != nil { + return + } + + prevSource := p.currentSource + p.currentSource = dmsBindsPath + baseDir := filepath.Dir(dmsBindsPath) + p.processNodes(doc.Nodes, section, baseDir) + p.currentSource = prevSource + p.dmsProcessed = true +} + func (p *NiriParser) finalizeBinds() []NiriKeyBinding { binds := make([]NiriKeyBinding, 0, len(p.bindOrder)) for _, key := range p.bindOrder { @@ -67,6 +110,20 @@ func (p *NiriParser) finalizeBinds() []NiriKeyBinding { func (p *NiriParser) addBind(kb *NiriKeyBinding) { key := p.formatBindKey(kb) + isDMSBind := strings.Contains(kb.Source, "dms/binds.kdl") + + if isDMSBind { + p.dmsBindKeys[key] = true + p.dmsBindMap[key] = kb + } else if p.dmsBindKeys[key] { + p.bindsAfterDMS++ + p.conflictingConfigs[key] = kb + p.configBindKeys[key] = true + return + } else { + p.configBindKeys[key] = true + } + if _, exists := p.bindMap[key]; !exists { p.bindOrder = append(p.bindOrder, key) } @@ -105,9 +162,11 @@ func (p *NiriParser) parseFile(filePath, sectionName string) (*NiriSection, erro Name: sectionName, } + prevSource := p.currentSource p.currentSource = absPath baseDir := filepath.Dir(absPath) p.processNodes(doc.Nodes, section, baseDir) + p.currentSource = prevSource return section, nil } @@ -133,8 +192,13 @@ func (p *NiriParser) handleInclude(node *document.Node, section *NiriSection, ba } includePath := strings.Trim(node.Arguments[0].String(), "\"") - if includePath == "dms/binds.kdl" || strings.HasSuffix(includePath, "/dms/binds.kdl") { + isDMSInclude := includePath == "dms/binds.kdl" || strings.HasSuffix(includePath, "/dms/binds.kdl") + + p.includeCount++ + if isDMSInclude { p.dmsBindsIncluded = true + p.dmsIncludePos = p.includeCount + p.bindsBeforeDMS = len(p.bindMap) } fullPath := filepath.Join(baseDir, includePath) @@ -142,6 +206,10 @@ func (p *NiriParser) handleInclude(node *document.Node, section *NiriSection, ba fullPath = includePath } + if isDMSInclude { + p.dmsProcessed = true + } + includedSection, err := p.parseFile(fullPath, "") if err != nil { return @@ -230,8 +298,49 @@ func (p *NiriParser) parseKeyCombo(combo string) ([]string, string) { } type NiriParseResult struct { - Section *NiriSection - DMSBindsIncluded bool + Section *NiriSection + DMSBindsIncluded bool + DMSStatus *DMSBindsStatusInfo + ConflictingConfigs map[string]*NiriKeyBinding +} + +type DMSBindsStatusInfo struct { + Exists bool + Included bool + IncludePosition int + TotalIncludes int + BindsAfterDMS int + Effective bool + OverriddenBy int + StatusMessage string +} + +func (p *NiriParser) buildDMSStatus() *DMSBindsStatusInfo { + status := &DMSBindsStatusInfo{ + Exists: p.dmsBindsExists, + Included: p.dmsBindsIncluded, + IncludePosition: p.dmsIncludePos, + TotalIncludes: p.includeCount, + BindsAfterDMS: p.bindsAfterDMS, + } + + switch { + case !p.dmsBindsExists: + status.Effective = false + status.StatusMessage = "dms/binds.kdl does not exist" + case !p.dmsBindsIncluded: + status.Effective = false + status.StatusMessage = "dms/binds.kdl is not included in config.kdl" + case p.bindsAfterDMS > 0: + status.Effective = true + status.OverriddenBy = p.bindsAfterDMS + status.StatusMessage = "Some DMS binds may be overridden by config binds" + default: + status.Effective = true + status.StatusMessage = "DMS binds are active" + } + + return status } func ParseNiriKeys(configDir string) (*NiriParseResult, error) { @@ -241,7 +350,9 @@ func ParseNiriKeys(configDir string) (*NiriParseResult, error) { return nil, err } return &NiriParseResult{ - Section: section, - DMSBindsIncluded: parser.HasDMSBindsIncluded(), + Section: section, + DMSBindsIncluded: parser.HasDMSBindsIncluded(), + DMSStatus: parser.buildDMSStatus(), + ConflictingConfigs: parser.conflictingConfigs, }, nil } diff --git a/core/internal/keybinds/types.go b/core/internal/keybinds/types.go index 2509fca4..6989d6a2 100644 --- a/core/internal/keybinds/types.go +++ b/core/internal/keybinds/types.go @@ -1,11 +1,23 @@ package keybinds type Keybind struct { - Key string `json:"key"` - Description string `json:"desc"` - Action string `json:"action,omitempty"` - Subcategory string `json:"subcat,omitempty"` - Source string `json:"source,omitempty"` + Key string `json:"key"` + Description string `json:"desc"` + Action string `json:"action,omitempty"` + Subcategory string `json:"subcat,omitempty"` + Source string `json:"source,omitempty"` + Conflict *Keybind `json:"conflict,omitempty"` +} + +type DMSBindsStatus struct { + Exists bool `json:"exists"` + Included bool `json:"included"` + IncludePosition int `json:"includePosition"` + TotalIncludes int `json:"totalIncludes"` + BindsAfterDMS int `json:"bindsAfterDms"` + Effective bool `json:"effective"` + OverriddenBy int `json:"overriddenBy"` + StatusMessage string `json:"statusMessage"` } type CheatSheet struct { @@ -13,6 +25,7 @@ type CheatSheet struct { Provider string `json:"provider"` Binds map[string][]Keybind `json:"binds"` DMSBindsIncluded bool `json:"dmsBindsIncluded"` + DMSStatus *DMSBindsStatus `json:"dmsStatus,omitempty"` } type Provider interface { diff --git a/quickshell/Modules/Settings/KeybindsTab.qml b/quickshell/Modules/Settings/KeybindsTab.qml index 932da674..046d3cec 100644 --- a/quickshell/Modules/Settings/KeybindsTab.qml +++ b/quickshell/Modules/Settings/KeybindsTab.qml @@ -18,6 +18,9 @@ Item { property int _lastDataVersion: -1 property var _cachedCategories: [] property var _filteredBinds: [] + property real _savedScrollY: 0 + property bool _preserveScroll: false + property string _editingKey: "" function _updateFiltered() { const allBinds = KeybindsService.getFlatBinds(); @@ -87,6 +90,9 @@ Item { function saveNewBind(bindData) { KeybindsService.saveBind("", bindData); showingNewBind = false; + selectedCategory = ""; + _editingKey = bindData.key; + expandedKey = bindData.action; } function scrollToTop() { @@ -102,9 +108,24 @@ Item { Connections { target: KeybindsService function onBindsLoaded() { + const savedY = keybindsTab._savedScrollY; + const wasPreserving = keybindsTab._preserveScroll; keybindsTab._lastDataVersion = KeybindsService._dataVersion; keybindsTab._updateCategories(); keybindsTab._updateFiltered(); + keybindsTab._preserveScroll = false; + if (wasPreserving) + Qt.callLater(() => flickable.contentY = savedY); + } + function onBindSaved(key) { + keybindsTab._savedScrollY = flickable.contentY; + keybindsTab._preserveScroll = true; + keybindsTab._editingKey = key; + } + function onBindRemoved(key) { + keybindsTab._savedScrollY = flickable.contentY; + keybindsTab._preserveScroll = true; + keybindsTab._editingKey = ""; } } @@ -228,14 +249,33 @@ Item { } StyledRect { + id: warningBox width: Math.min(650, parent.width - Theme.spacingL * 2) height: warningSection.implicitHeight + Theme.spacingL * 2 anchors.horizontalCenter: parent.horizontalCenter radius: Theme.cornerRadius - color: Theme.withAlpha(Theme.error, 0.15) - border.color: Theme.withAlpha(Theme.error, 0.3) + + readonly property var status: KeybindsService.dmsStatus + readonly property bool showError: !status.included && status.exists + readonly property bool showWarning: status.included && status.overriddenBy > 0 + readonly property bool showSetup: !status.exists + + color: { + if (showError || showSetup) + return Theme.withAlpha(Theme.error, 0.15); + if (showWarning) + return Theme.withAlpha(Theme.warning ?? Theme.tertiary, 0.15); + return "transparent"; + } + border.color: { + if (showError || showSetup) + return Theme.withAlpha(Theme.error, 0.3); + if (showWarning) + return Theme.withAlpha(Theme.warning ?? Theme.tertiary, 0.3); + return "transparent"; + } border.width: 1 - visible: !KeybindsService.dmsBindsIncluded && !KeybindsService.loading + visible: (showError || showWarning || showSetup) && !KeybindsService.loading Column { id: warningSection @@ -248,26 +288,44 @@ Item { spacing: Theme.spacingM DankIcon { - name: "warning" + name: warningBox.showWarning ? "info" : "warning" size: Theme.iconSize - color: Theme.error + color: warningBox.showWarning ? (Theme.warning ?? Theme.tertiary) : Theme.error anchors.verticalCenter: parent.verticalCenter } Column { - width: parent.width - Theme.iconSize - 100 - Theme.spacingM * 2 + width: parent.width - Theme.iconSize - (fixButton.visible ? fixButton.width + Theme.spacingM : 0) - Theme.spacingM spacing: Theme.spacingXS anchors.verticalCenter: parent.verticalCenter StyledText { - text: I18n.tr("Binds Include Missing") + text: { + if (warningBox.showSetup) + return I18n.tr("First Time Setup"); + if (warningBox.showError) + return I18n.tr("Binds Include Missing"); + if (warningBox.showWarning) + return I18n.tr("Possible Override Conflicts"); + return ""; + } font.pixelSize: Theme.fontSizeMedium font.weight: Font.Medium - color: Theme.error + color: warningBox.showWarning ? (Theme.warning ?? Theme.tertiary) : Theme.error } StyledText { - text: I18n.tr("dms/binds.kdl is not included in config.kdl. Custom keybinds will not work until this is fixed.") + text: { + if (warningBox.showSetup) + return I18n.tr("Click 'Setup' to create dms/binds.kdl and add include to config.kdl."); + if (warningBox.showError) + return I18n.tr("dms/binds.kdl exists but is not included in config.kdl. Custom keybinds will not work until this is fixed."); + if (warningBox.showWarning) { + const count = warningBox.status.overriddenBy; + return I18n.tr("%1 DMS bind(s) may be overridden by config binds that come after the include.").arg(count); + } + return ""; + } font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText wrapMode: Text.WordWrap @@ -280,12 +338,19 @@ Item { width: fixButtonText.implicitWidth + Theme.spacingL * 2 height: 36 radius: Theme.cornerRadius + visible: warningBox.showError || warningBox.showSetup color: KeybindsService.fixing ? Theme.withAlpha(Theme.error, 0.6) : Theme.error anchors.verticalCenter: parent.verticalCenter StyledText { id: fixButtonText - text: KeybindsService.fixing ? I18n.tr("Fixing...") : I18n.tr("Fix Now") + text: { + if (KeybindsService.fixing) + return I18n.tr("Fixing..."); + if (warningBox.showSetup) + return I18n.tr("Setup"); + return I18n.tr("Fix Now"); + } font.pixelSize: Theme.fontSizeSmall font.weight: Font.Medium color: Theme.surface @@ -532,6 +597,7 @@ Item { anchors.horizontalCenter: parent.horizontalCenter bindData: modelData isExpanded: keybindsTab.expandedKey === modelData.action + restoreKey: isExpanded ? keybindsTab._editingKey : "" panelWindow: keybindsTab.parentModal onToggleExpand: keybindsTab.toggleExpanded(modelData.action) onSaveBind: (originalKey, newData) => { @@ -539,6 +605,7 @@ Item { keybindsTab.expandedKey = modelData.action; } onRemoveBind: key => KeybindsService.removeBind(key) + onRestoreKeyConsumed: keybindsTab._editingKey = "" } } } diff --git a/quickshell/Services/KeybindsService.qml b/quickshell/Services/KeybindsService.qml index 0a4c49d5..dd4eee44 100644 --- a/quickshell/Services/KeybindsService.qml +++ b/quickshell/Services/KeybindsService.qml @@ -28,6 +28,17 @@ Singleton { property string lastError: "" property bool dmsBindsIncluded: true + property var dmsStatus: ({ + exists: true, + included: true, + includePosition: -1, + totalIncludes: 0, + bindsAfterDms: 0, + effective: true, + overriddenBy: 0, + statusMessage: "" + }) + property var _rawData: null property var keybinds: ({}) property var _allBinds: ({}) @@ -60,6 +71,14 @@ Singleton { } } + Connections { + target: NiriService + enabled: CompositorService.isNiri + function onConfigReloaded() { + Qt.callLater(root.loadBinds, false); + } + } + Process { id: loadProcess running: false @@ -178,7 +197,7 @@ Singleton { } function loadBinds(showLoading) { - if (loading || !available) + if (loadProcess.running || !available) return; const hasData = Object.keys(_allBinds).length > 0; loading = showLoading !== false && !hasData; @@ -188,8 +207,22 @@ Singleton { function _processData() { keybinds = _rawData || {}; - if (currentProvider === "niri") + if (currentProvider === "niri") { dmsBindsIncluded = _rawData?.dmsBindsIncluded ?? true; + const status = _rawData?.dmsStatus; + if (status) { + dmsStatus = { + exists: status.exists ?? true, + included: status.included ?? true, + includePosition: status.includePosition ?? -1, + totalIncludes: status.totalIncludes ?? 0, + bindsAfterDms: status.bindsAfterDms ?? 0, + effective: status.effective ?? true, + overriddenBy: status.overriddenBy ?? 0, + statusMessage: status.statusMessage ?? "" + }; + } + } if (!_rawData?.binds) { _allBinds = {}; @@ -239,12 +272,15 @@ Singleton { actionMap[action].keys.push(keyData); if (!actionMap[action].desc && bind.desc) actionMap[action].desc = bind.desc; + if (!actionMap[action].conflict && bind.conflict) + actionMap[action].conflict = bind.conflict; } else { const entry = { category: category, action: action, desc: bind.desc || "", - keys: [keyData] + keys: [keyData], + conflict: bind.conflict || null }; actionMap[action] = entry; grouped.push(entry); diff --git a/quickshell/Services/NiriService.qml b/quickshell/Services/NiriService.qml index 5498d382..259c48b9 100644 --- a/quickshell/Services/NiriService.qml +++ b/quickshell/Services/NiriService.qml @@ -39,6 +39,7 @@ Singleton { property string pendingScreenshotPath: "" signal windowUrgentChanged + signal configReloaded function setWorkspaces(newMap) { root.workspaces = newMap; @@ -508,16 +509,19 @@ Singleton { function handleConfigLoaded(data) { if (data.failed) { validateProcess.running = true; - } else { - configValidationOutput = ""; - ToastService.dismissCategory("niri-config"); - fetchOutputs(); - if (hasInitialConnection && !suppressConfigToast && !suppressNextConfigToast && !matugenSuppression) { - ToastService.showInfo("niri: config reloaded", "", "", "niri-config"); - } else if (suppressNextConfigToast) { - suppressNextConfigToast = false; - suppressResetTimer.stop(); - } + return; + } + + configValidationOutput = ""; + ToastService.dismissCategory("niri-config"); + fetchOutputs(); + configReloaded(); + + if (hasInitialConnection && !suppressConfigToast && !suppressNextConfigToast && !matugenSuppression) { + ToastService.showInfo("niri: config reloaded", "", "", "niri-config"); + } else if (suppressNextConfigToast) { + suppressNextConfigToast = false; + suppressResetTimer.stop(); } if (!hasInitialConnection) { diff --git a/quickshell/Widgets/KeybindItem.qml b/quickshell/Widgets/KeybindItem.qml index a65b5037..914fc2c4 100644 --- a/quickshell/Widgets/KeybindItem.qml +++ b/quickshell/Widgets/KeybindItem.qml @@ -17,6 +17,7 @@ Item { property var panelWindow: null property bool recording: false property bool isNew: false + property string restoreKey: "" property int editingKeyIndex: -1 property string editKey: "" @@ -44,6 +45,8 @@ Item { } return false; } + readonly property var configConflict: bindData.conflict || null + readonly property bool hasConfigConflict: configConflict !== null readonly property string _originalKey: editingKeyIndex >= 0 && editingKeyIndex < keys.length ? keys[editingKeyIndex].key : "" readonly property var _conflicts: editKey ? KeyUtils.getConflictingBinds(editKey, bindData.action, KeybindsService.getFlatBinds()) : [] readonly property bool hasConflict: _conflicts.length > 0 @@ -52,6 +55,7 @@ Item { signal saveBind(string originalKey, var newData) signal removeBind(string key) signal cancelEdit + signal restoreKeyConsumed implicitHeight: contentColumn.implicitHeight height: implicitHeight @@ -59,8 +63,37 @@ Item { Component.onDestruction: _destroyShortcutInhibitor() onIsExpandedChanged: { - if (isExpanded) + if (!isExpanded) + return; + if (restoreKey) { + restoreToKey(restoreKey); + restoreKeyConsumed(); + } else { resetEdits(); + } + } + + onRestoreKeyChanged: { + if (!isExpanded || !restoreKey) + return; + restoreToKey(restoreKey); + restoreKeyConsumed(); + } + + function restoreToKey(keyToFind) { + for (let i = 0; i < keys.length; i++) { + if (keys[i].key === keyToFind) { + editingKeyIndex = i; + editKey = keyToFind; + editAction = bindData.action || ""; + editDesc = bindData.desc || ""; + hasChanges = false; + _actionType = Actions.getActionType(editAction); + useCustomCompositor = _actionType === "compositor" && !Actions.isKnownCompositorAction(editAction); + return; + } + } + resetEdits(); } onEditActionChanged: { @@ -276,7 +309,21 @@ Item { text: I18n.tr("Override") font.pixelSize: Theme.fontSizeSmall color: Theme.primary - visible: root.hasOverride + visible: root.hasOverride && !root.hasConfigConflict + } + + DankIcon { + name: "warning" + size: 14 + color: Theme.warning ?? Theme.tertiary + visible: root.hasConfigConflict + } + + StyledText { + text: I18n.tr("Overridden by config") + font.pixelSize: Theme.fontSizeSmall + color: Theme.warning ?? Theme.tertiary + visible: root.hasConfigConflict } Item { @@ -334,6 +381,58 @@ Item { anchors.margins: Theme.spacingL spacing: Theme.spacingM + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: conflictColumn.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.warning ?? Theme.tertiary, 0.15) + border.color: Theme.withAlpha(Theme.warning ?? Theme.tertiary, 0.3) + border.width: 1 + visible: root.hasConfigConflict + + Column { + id: conflictColumn + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingS + + RowLayout { + width: parent.width + spacing: Theme.spacingS + + DankIcon { + name: "warning" + size: 16 + color: Theme.warning ?? Theme.tertiary + } + + StyledText { + text: I18n.tr("This bind is overridden by config.kdl") + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.warning ?? Theme.tertiary + Layout.fillWidth: true + } + } + + StyledText { + text: I18n.tr("Config action: %1").arg(root.configConflict?.action ?? "") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + wrapMode: Text.WordWrap + } + + StyledText { + text: I18n.tr("To use this DMS bind, remove or change the keybind in your config.kdl") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + wrapMode: Text.WordWrap + } + } + } + RowLayout { Layout.fillWidth: true spacing: Theme.spacingM