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) }