From 6f2da98dca6d93f5f88d29d4abcb7a136b38e6c0 Mon Sep 17 00:00:00 2001 From: Robertkill Date: Wed, 15 Apr 2026 11:31:19 +0800 Subject: [PATCH] fix(keybinding): resolve shortcut conflicts and improve Wayland support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: 1. Shortcut modification via control-center conflicts with dconfig listener 2. System shortcuts not properly synchronized to KWin on Wayland 3. Special shortcuts (screenshotWindow, launcher, systemMonitor) need specific KWin formats Solution: 1. Add modifyingShortcuts set to track API modifications, skip dconfig callback during modification 2. Sync system shortcut changes to KWin immediately when dconfig changes 3. Restore hardcoded accelJson for special shortcuts with KWin-compatible formats 4. Fix single keystroke replacement logic (clear existing except Delete shortcuts) 5. Add enhanced logging for debugging Influence: 1. Test shortcut modification via control-center on Wayland 2. Test system shortcuts (screenshot, launcher, system monitor) on Wayland 3. Test custom shortcut add/delete/clear operations 4. Verify no duplicate shortcut registrations after modification 5. Verify dconfig and KWin stay in sync fix: 修复快捷键冲突和 Wayland 支持问题 问题: 1. 控制中心修改快捷键与 dconfig 监听回调产生竞争冲突 2. Wayland 下系统快捷键未正确同步到 KWin 3. 特殊快捷键(截图窗口、启动器、系统监视器)需要特定 KWin 格式 解决方案: 1. 添加 modifyingShortcuts 集合跟踪 API 修改,修改期间跳过 dconfig 回调 2. dconfig 变更时立即同步系统快捷键到 KWin 3. 恢复特殊快捷键的硬编码 accelJson,使用 KWin 兼容格式 4. 修复单键替换逻辑(清空已有快捷键,Delete 类保留双绑) 5. 添加详细日志便于调试 影响范围: 1. 测试 Wayland 下控制中心修改快捷键 2. 测试 Wayland 下系统快捷键(截图、启动器、系统监视器) 3. 测试自定义快捷键的增删清操作 4. 验证修改后无重复快捷键注册 5. 验证 dconfig 和 KWin 保持同步 PMS: BUG-355747 --- keybinding1/manager.go | 63 +++++++++++++++++++++++ keybinding1/manager_ifc.go | 57 +++++++++++++++++--- keybinding1/shortcuts/shortcut_manager.go | 60 +++++++++++++++------ 3 files changed, 156 insertions(+), 24 deletions(-) diff --git a/keybinding1/manager.go b/keybinding1/manager.go index 08cef5793..8b83c3095 100644 --- a/keybinding1/manager.go +++ b/keybinding1/manager.go @@ -18,6 +18,7 @@ import ( "os/exec" "path/filepath" "strings" + "sync" "time" dbus "github.com/godbus/dbus/v5" @@ -128,6 +129,9 @@ type Manager struct { sessionSigLoop *dbusutil.SignalLoop systemSigLoop *dbusutil.SignalLoop //startManager sessionmanager.StartManager + + // 记录当前正在通过 API 修改的 shortcuts,避免和 dconfig 监听竞争 + modifyingShortcuts *StringSet sessionManager sessionmanager.SessionManager airplane airplanemode.AirplaneMode networkmanager networkmanager.Manager @@ -238,6 +242,7 @@ func newManager(service *dbusutil.Service) (*Manager, error) { conn: conn, keySymbols: keysyms.NewKeySymbols(conn), handlers: make([]shortcuts.KeyEventFunc, shortcuts.ActionTypeCount), + modifyingShortcuts: NewStringSet(), } m.sessionSigLoop = dbusutil.NewSignalLoop(sessionBus, 10) @@ -663,6 +668,7 @@ func (m *Manager) listenGlobalAccel(sessionBus *dbus.Conn) error { m.shortcutKey = sig.Body[0].(string) m.shortcutKeyCmd = sig.Body[1].(string) shortId := kwinSysActionCmdMap[m.shortcutKeyCmd] + logger.Infof("[keybinding][wayland] globalShortcutPressed component=%q action=%q mappedId=%q", m.shortcutKey, m.shortcutKeyCmd, shortId) if shortId != "" && m.DisabledSystemShortcutsList.Contains(shortId) { logger.Warningf("shortcut id: %s is disabled", shortId) return @@ -678,6 +684,7 @@ func (m *Manager) listenGlobalAccel(sessionBus *dbus.Conn) error { if m.shortcutCmd == "" { m.shortcutCmd = m.shortcutManager.WaylandCustomShortCutMap[m.shortcutKeyCmd] } + logger.Infof("[keybinding][wayland] resolved action=%q mappedId=%q cmd=%q", m.shortcutKeyCmd, kwinSysActionCmdMap[m.shortcutKeyCmd], m.shortcutCmd) logger.Debug("WaylandCustomShortCutMap", m.shortcutCmd) if m.shortcutCmd == "" { m.handleKeyEventByWayland(waylandMediaIdMap[m.shortcutKeyCmd]) @@ -1239,6 +1246,12 @@ func (m *Manager) listenDConfigChanged(config configManager.Manager, type0 int32 if !m.enableListenDConfig { return } + // 如果当前正在通过 API 修改这个 shortcut,跳过监听回调 + // 避免控制中心的 Add/Clear 操作和 dconfig 监听之间的竞争 + if m.modifyingShortcuts != nil && m.modifyingShortcuts.Has(key) { + logger.Debugf("listenDConfigChanged skipping %s, currently being modified via API", key) + return + } shortcut := m.shortcutManager.GetByIdType(key, type0) if shortcut == nil { @@ -1256,6 +1269,24 @@ func (m *Manager) listenDConfigChanged(config configManager.Manager, type0 int32 return } m.shortcutManager.ModifyShortcutKeystrokes(shortcut, shortcuts.ParseKeystrokes(keystrokes)) + if _useWayland && type0 == shortcuts.ShortcutTypeSystem { + keystrokes = shortcuts.NormalizeSystemKeystrokesForKWin(key, keystrokes) + accelJson, err := json.Marshal(struct { + Id string `json:"Id"` + Keystrokes []string `json:"Accels"` + }{ + Id: key, + Keystrokes: keystrokes, + }) + if err != nil { + logger.Warning("failed to marshal KWin accel json:", err) + } else { + ok, setErr := m.wm.SetAccel(0, string(accelJson)) + if !ok { + logger.Warning("failed to update KWin accel:", key, keystrokes, setErr) + } + } + } m.emitShortcutSignal(shortcutSignalChanged, shortcut) }) } @@ -1426,3 +1457,35 @@ func (m *Manager) eliminateKeystrokeConflict() { m.shortcutManager.ConflictingKeystrokes = nil m.shortcutManager.EliminateConflictDone = true } + +// StringSet 是一个简单的字符串集合,用于并发安全的标记 + +type StringSet struct { + mu sync.RWMutex + m map[string]struct{} +} + +func NewStringSet() *StringSet { + return &StringSet{ + m: make(map[string]struct{}), + } +} + +func (s *StringSet) Add(key string) { + s.mu.Lock() + defer s.mu.Unlock() + s.m[key] = struct{}{} +} + +func (s *StringSet) Remove(key string) { + s.mu.Lock() + defer s.mu.Unlock() + delete(s.m, key) +} + +func (s *StringSet) Has(key string) bool { + s.mu.RLock() + defer s.mu.RUnlock() + _, ok := s.m[key] + return ok +} diff --git a/keybinding1/manager_ifc.go b/keybinding1/manager_ifc.go index 641810642..184e77db2 100644 --- a/keybinding1/manager_ifc.go +++ b/keybinding1/manager_ifc.go @@ -73,21 +73,31 @@ func (m *Manager) setAccelForWayland(config configManager.Manager, wmObj wm.Wm) for _, id := range keys { var accelJson string var err error + + // 特殊处理:这些快捷键需要固定的 KWin 格式 if id == "screenshotWindow" { - accelJson = `{"Id":"screenshotWindow","Accels":["SysReq"]}` //+ Alt+print对应kwin识别的键SysReq + accelJson = `{"Id":"screenshotWindow","Accels":["SysReq"]}` // Alt+print对应kwin识别的键SysReq } else if id == "launcher" { accelJson = `{"Id":"launcher","Accels":["Super_L"]}` // wayland左右super对应的都是Super_L - } else if id == "system_monitor" { - accelJson = `{"Id":"system_monitor","Accels":["Escape"]}` + } else if id == "systemMonitor" { + accelJson = `{"Id":"systemMonitor","Accels":["Escape"]}` } else { - KeystrokesValue, err := config.Value(0, id) + keystrokesValue, err := config.Value(0, id) if err != nil { logger.Warning("failed to get value:", err) continue } + + keystrokes, err := shortcuts.ConvertToStringSliceForWayland(keystrokesValue.Value()) + if err != nil { + logger.Warning("failed to convert keystrokes:", err) + continue + } + keystrokes = shortcuts.NormalizeSystemKeystrokesForKWin(id, keystrokes) + accelJson, err = util.MarshalJSON(util.KWinAccel{ Id: id, - Keystrokes: KeystrokesValue.Value().([]string), + Keystrokes: keystrokes, }) if err != nil { logger.Warning("failed to get json:", err) @@ -95,9 +105,10 @@ func (m *Manager) setAccelForWayland(config configManager.Manager, wmObj wm.Wm) } } + logger.Infof("[keybinding][wayland] setAccelForWayland id=%q", id) ok, err := wmObj.SetAccel(0, accelJson) if !ok { - logger.Warning("failed to set KWin accels:", err) + logger.Warning("failed to set KWin accels:", id, err) } } } @@ -288,6 +299,11 @@ func (m *Manager) DeleteCustomShortcut(id string) *dbus.Error { func (m *Manager) ClearShortcutKeystrokes(id string, type0 int32) *dbus.Error { logger.Debug("ClearShortcutKeystrokes", id, type0) + // 标记正在修改,避免 dconfig 监听回调干扰 + if m.modifyingShortcuts != nil { + m.modifyingShortcuts.Add(id) + defer m.modifyingShortcuts.Remove(id) + } shortcut := m.shortcutManager.GetByIdType(id, type0) if shortcut == nil { return dbusutil.ToError(ErrShortcutNotFound{id, type0}) @@ -417,6 +433,11 @@ func (m *Manager) ModifyCustomShortcut(id, name, cmd, keystroke string) *dbus.Er func (m *Manager) AddShortcutKeystroke(id string, type0 int32, keystroke string) *dbus.Error { logger.Debug("AddShortcutKeystroke", id, type0, keystroke) + // 标记正在修改,避免 dconfig 监听回调干扰 + if m.modifyingShortcuts != nil { + m.modifyingShortcuts.Add(id) + defer m.modifyingShortcuts.Remove(id) + } shortcut := m.shortcutManager.GetByIdType(id, type0) if shortcut == nil { return dbusutil.ToError(ErrShortcutNotFound{id, type0}) @@ -467,6 +488,25 @@ func (m *Manager) AddShortcutKeystroke(id string, type0 int32, keystroke string) } } + // 对于系统快捷键,如果已有 keystrokes 且不是 Delete 相关,则清空 + // Delete 类快捷键需要保留双绑(Delete + KP_Delete) + if type0 == shortcuts.ShortcutTypeSystem && len(shortcut.GetKeystrokes()) > 0 { + // 检查是否涉及 Delete 键 + hasDelete := false + for _, ks := range shortcut.GetKeystrokes() { + if strings.Contains(ks.String(), "Delete") { + hasDelete = true + break + } + } + if !hasDelete { + // 非 Delete 类系统快捷键,清空后添加 + m.shortcutManager.ModifyShortcutKeystrokes(shortcut, nil) + } else { + // Delete 类快捷键保留双绑 + } + } + // 添加所有 keystroke for _, ksToAdd := range keystrokesToAdd { m.shortcutManager.AddShortcutKeystroke(shortcut, ksToAdd) @@ -484,7 +524,7 @@ func (m *Manager) AddShortcutKeystroke(id string, type0 int32, keystroke string) } func (m *Manager) DeleteShortcutKeystroke(id string, type0 int32, keystroke string) *dbus.Error { - logger.Debug("DeleteShortcutKeystroke", id, type0, keystroke) + logger.Infof("[keybinding] DeleteShortcutKeystroke id=%q type=%d keystroke=%q", id, type0, keystroke) shortcut := m.shortcutManager.GetByIdType(id, type0) if shortcut == nil { return dbusutil.ToError(ErrShortcutNotFound{id, type0}) @@ -500,11 +540,14 @@ func (m *Manager) DeleteShortcutKeystroke(id string, type0 int32, keystroke stri } logger.Debug("keystroke:", ks.DebugString()) + logger.Infof("[keybinding] DeleteShortcutKeystroke before delete: id=%q currentKeystrokes=%v", id, shortcut.GetKeystrokes()) m.shortcutManager.DeleteShortcutKeystroke(shortcut, ks) + logger.Infof("[keybinding] DeleteShortcutKeystroke after delete: id=%q remainingKeystrokes=%v", id, shortcut.GetKeystrokes()) err = shortcut.SaveKeystrokes() if err != nil { return dbusutil.ToError(err) } + logger.Infof("[keybinding] DeleteShortcutKeystroke after save: id=%q savedKeystrokes=%v", id, shortcut.GetKeystrokes()) if shortcut.ShouldEmitSignalChanged() { m.emitShortcutSignal(shortcutSignalChanged, shortcut) } diff --git a/keybinding1/shortcuts/shortcut_manager.go b/keybinding1/shortcuts/shortcut_manager.go index 0a266d981..f0228d277 100644 --- a/keybinding1/shortcuts/shortcut_manager.go +++ b/keybinding1/shortcuts/shortcut_manager.go @@ -170,6 +170,10 @@ func convertToStringSlice(value interface{}) ([]string, error) { return nil, fmt.Errorf("unsupported type for string slice conversion: %T", value) } +func ConvertToStringSliceForWayland(value interface{}) ([]string, error) { + return convertToStringSlice(value) +} + func NewShortcutManager(conn *x.Conn, keySymbols *keysyms.KeySymbols, eventCb KeyEventFunc) *ShortcutManager { setUseWayland(strings.Contains(os.Getenv("XDG_SESSION_TYPE"), "wayland")) ss := &ShortcutManager{ @@ -541,9 +545,10 @@ func (sm *ShortcutManager) storeConflictingKeystroke(ks *Keystroke) { func (sm *ShortcutManager) grabKeystroke(shortcut Shortcut, ks *Keystroke, dummy bool) { keyList, err := ks.ToKeyList(sm.keySymbols) if err != nil { - logger.Debugf("grabKeystroke failed, shortcut: %v, ks: %v, err: %v", shortcut.GetId(), ks, err) + logger.Warningf("[keybinding] grabKeystroke failed, shortcut: %v, ks: %v, err: %v", shortcut.GetId(), ks, err) return } + logger.Debugf("grabKeystroke shortcut: %s, ks: %s, dummy: %v", shortcut.GetId(), ks, dummy) var conflictCount int var idx = -1 @@ -1342,6 +1347,7 @@ func setShortForWayland(shortcut Shortcut, wmObj wm.Wm) (bool, error) { } keystrokesStrv := shortcut.getKeystrokesStrv() logger.Debugf("Id: %+v, keystrokesStrv: %+v", id, keystrokesStrv) + logger.Infof("[keybinding][wayland] setShortForWayland id=%q keystrokes=%v isCustom=%v", id, keystrokesStrv, isCustom) accelJson, err := util.MarshalJSON(util.KWinAccel{ Id: id, Keystrokes: keystrokesStrv, @@ -1469,6 +1475,27 @@ func (sm *ShortcutManager) AddKWinForWayland(wmObj wm.Wm) { } } +func NormalizeSystemKeystrokesForKWin(id string, keystrokes []string) []string { + result := append([]string(nil), keystrokes...) + + for i := 0; i < len(result); i++ { + result[i] = strings.Replace(result[i], "Del", "Delete", 1) + result[i] = strings.Replace(result[i], "Esc", "Escape", 1) + } + + if id == "screenshotWindow" { + return []string{"SysReq"} // Alt+Print 对应 kwin 识别的键 SysReq + } + if id == "launcher" { + return []string{"Super_L"} // wayland左右super对应的都是Super_L + } + if id == "systemMonitor" { + return []string{"Escape"} // KWin需要这个特定格式 + } + + return result +} + func (sm *ShortcutManager) AddSystemToKwin(wmObj wm.Wm) { logger.Debug("AddSystemToKwin") idNameMap := getSystemIdNameMap() @@ -1493,29 +1520,28 @@ func (sm *ShortcutManager) AddSystemToKwin(wmObj wm.Wm) { logger.Warning("Failed to convert keystrokes:", err) continue } - accelJson, err := util.MarshalJSON(util.KWinAccel{ - Id: id, - Keystrokes: keystrokes, - }) + keystrokes = NormalizeSystemKeystrokesForKWin(id, keystrokes) + var accelJson string if id == "screenshotWindow" { accelJson = `{"Id":"screenshotWindow","Accels":["SysReq"]}` //+ Alt+print对应kwin识别的键SysReq - } - if id == "launcher" { + } else if id == "launcher" { accelJson = `{"Id":"launcher","Accels":["Super_L"]}` - } - if id == "system_monitor" { - accelJson = `{"Id":"system_monitor","Accels":["Escape"]}` - } - if err != nil { - logger.Warning("failed to get json:", err) - continue + } else if id == "systemMonitor" { + accelJson = `{"Id":"systemMonitor","Accels":["Escape"]}` + } else { + accelJson, err = util.MarshalJSON(util.KWinAccel{ + Id: id, + Keystrokes: keystrokes, + }) + if err != nil { + logger.Warning("failed to get json:", err) + continue + } } ok, err := wmObj.SetAccel(0, accelJson) if !ok { - - logKeystrokes, _ := convertToStringSlice(strokesValue.Value()) - logger.Warning("failed to set KWin accels:", id, logKeystrokes, err) + logger.Warning("failed to set KWin accels:", id, keystrokes, err) } sm.AddSystemById(wmObj, id) }