199 lines
7.4 KiB
Python
199 lines
7.4 KiB
Python
from pynput import keyboard
|
||
from PySide2.QtCore import QMutex
|
||
from time import time
|
||
|
||
from umi_log import logger
|
||
from ...platform import Platform
|
||
from ..pubsub_service import PubSubService
|
||
|
||
|
||
# 按键转换器
|
||
class _KeyTranslator:
|
||
# 回调函数的 KeyCode 类型 转为键名字符串
|
||
@staticmethod
|
||
def key2name(key):
|
||
return Platform.getKeyName(key)
|
||
|
||
# 键名字符串 转为KeyCode
|
||
@staticmethod
|
||
def name2key(char):
|
||
if hasattr(keyboard.Key, char): # 控制键返回Code
|
||
return getattr(keyboard.Key, char).value
|
||
else: # 非控制键返回自己
|
||
return char
|
||
|
||
# 多个键名的字符串 转为集合
|
||
@staticmethod
|
||
def names2set(char):
|
||
return set(char.split("+"))
|
||
|
||
# 集合转键名
|
||
def set2names(keys):
|
||
keys = keys.copy()
|
||
# 优先级键名
|
||
highPriority = ("win", "cmd", "shift", "ctrl", "alt")
|
||
names = ""
|
||
# 添加高优先级键
|
||
for h in highPriority:
|
||
if h in keys:
|
||
if names != "":
|
||
names += "+"
|
||
names += h
|
||
keys.discard(h)
|
||
# 添加剩下的键
|
||
for k in keys:
|
||
if names != "":
|
||
names += "+"
|
||
names += k
|
||
return names
|
||
|
||
|
||
# 热键控制器类
|
||
class _HotkeyController:
|
||
# ========================= 【接口】 =========================
|
||
|
||
# 添加一组快捷键,对应触发事件为title。 press 为0时按下触发,1抬起触发
|
||
def addHotkey(self, keysName, title, press=0):
|
||
self._start()
|
||
if press != 0 and press != 1:
|
||
return f"[Error] press只能为0按下或1抬起,不能为 {press} 。"
|
||
keySet = _KeyTranslator.names2set(keysName)
|
||
self._hotkeyMutex.lock()
|
||
kl = self._hotkeyList[press]
|
||
for k in kl: # 检测重复
|
||
if k["keySet"] == keySet: # 键集合相同
|
||
self._hotkeyMutex.unlock()
|
||
msg = "[Success] 注册事件相同的重复快捷键。"
|
||
if k["title"] != title: # 事件标题不同
|
||
msg = f'[Warning] Registering same hotkey. The existing event for {keysName} is {k["title"]}, new event is {title} .'
|
||
return msg
|
||
# 加入列表
|
||
kl.append({"keySet": keySet, "title": title})
|
||
self._hotkeyMutex.unlock()
|
||
return "[Success]"
|
||
|
||
# 移除一组快捷键,传入键名或事件之一
|
||
def delHotkey(self, keysName="", title="", press=0):
|
||
if press != 0 and press != 1:
|
||
logger.error(f"press只能为0按下或1抬起,不能为 {press} 。")
|
||
return
|
||
keySet = _KeyTranslator.names2set(keysName)
|
||
self._hotkeyMutex.lock()
|
||
kListOld = self._hotkeyList[press] # 旧列表
|
||
kListNew = [] # 新列表
|
||
for k in kListOld:
|
||
# 忽略键集合相同或标题相同
|
||
if (keysName and k["keySet"] == keySet) or (title and title == k["title"]):
|
||
pass
|
||
# 其余键写入新列表
|
||
else:
|
||
kListNew.append(k)
|
||
self._hotkeyList[press] = kListNew
|
||
self._hotkeyMutex.unlock()
|
||
|
||
# 开始录制快捷键。过程发送事件为runningTitle,完毕发送事件为finishTitle
|
||
def readHotkey(
|
||
self, runningTitle="<<readHotkeyRunning>>", finishTitle="<<readHotkeyFinish>>"
|
||
):
|
||
self._start()
|
||
if self._status == 1:
|
||
return (
|
||
"[Warning] Recording is running. 当前快捷键录制已在进行,不能同时录制!"
|
||
)
|
||
self._status = 1
|
||
self._readRunningTitle = runningTitle
|
||
self._readFinishTitle = finishTitle
|
||
return "[Success]"
|
||
|
||
# ========================= 【实现】 =========================
|
||
|
||
def __init__(self):
|
||
self._listener = None # 监听器
|
||
# 热键列表,[0]存放按下触发,[1]存放抬起触发
|
||
# 每个元素为:{"keySet":按键集合, "title":事件标题}
|
||
self._hotkeyList = [[], []]
|
||
self._hotkeyMutex = QMutex() # 热键列表的锁
|
||
self._status = 0 # 状态,0正常,1录制中
|
||
self._pressSet = set() # 当前已按下的按键集合
|
||
self._strict = True # 键集合相等的判定,T为严格,F为宽松
|
||
self._ttl = 30 # 长按键超时忽略时间,秒
|
||
self._ttlDict = {} # 存放当前已按下按键的超时时间
|
||
self._readRunningTitle = ""
|
||
self._readFinishTitle = ""
|
||
# 注册默认监听的热键
|
||
self.addHotkey("esc", "<<esc>>")
|
||
|
||
# 第一次注册热键时,启动监听
|
||
def _start(self):
|
||
if not self._listener:
|
||
self._listener = keyboard.Listener(
|
||
on_press=self._onPress, on_release=self._onRelease
|
||
)
|
||
self._listener.start()
|
||
|
||
# 按键按下的回调
|
||
def _onPress(self, key):
|
||
keyName = _KeyTranslator.key2name(key)
|
||
# 禁止重复触发
|
||
if keyName in self._pressSet:
|
||
return
|
||
self._checkTTL() # 检查超时
|
||
self._pressSet.add(keyName) # 加入集合
|
||
self._ttlDict[keyName] = time() + self._ttl # 记录超时时间
|
||
if self._status == 0: # 正常运行
|
||
self._checkKeyEvent(0, keyName) # 检查按下模式的快捷键
|
||
elif self._status == 1: # 录制中
|
||
self._readRunning()
|
||
|
||
# 按键抬起的回调
|
||
def _onRelease(self, key):
|
||
keyName = _KeyTranslator.key2name(key)
|
||
if not keyName in self._pressSet:
|
||
return
|
||
self._checkTTL() # 检查超时
|
||
if self._status == 0: # 正常运行
|
||
self._checkKeyEvent(1, keyName) # 检查抬起模式的快捷键
|
||
elif self._status == 1: # 录制结束
|
||
self._readFinish()
|
||
if keyName in self._pressSet:
|
||
self._pressSet.discard(keyName) # 从集合中删除
|
||
del self._ttlDict[keyName] # 删除超时时间
|
||
|
||
# 检查并触发按键事件
|
||
def _checkKeyEvent(self, press, nowKey):
|
||
# 对比每组按键集合。一致触发,则发送事件
|
||
if self._strict: # 严格模式,要求完全一致
|
||
for k in self._hotkeyList[press]:
|
||
if k["keySet"] == self._pressSet:
|
||
PubSubService.publish(k["title"])
|
||
else: # 宽松模式,只要求当前组合中包含指定按键,且当前按下的按键在指定按键中
|
||
for k in self._hotkeyList[press]:
|
||
if k["keySet"] <= self._pressSet and nowKey in k["keySet"]:
|
||
PubSubService.publish(k["title"])
|
||
|
||
# 更新录制
|
||
def _readRunning(self):
|
||
names = _KeyTranslator.set2names(self._pressSet)
|
||
PubSubService.publish(self._readRunningTitle, names)
|
||
|
||
# 录制结束
|
||
def _readFinish(self):
|
||
self._status = 0
|
||
if "esc" in self._pressSet: # 含esc,则为退出
|
||
PubSubService.publish(self._readFinishTitle, "")
|
||
else:
|
||
names = _KeyTranslator.set2names(self._pressSet)
|
||
PubSubService.publish(self._readFinishTitle, names)
|
||
|
||
# 检查已按键的超时时间。若超时,则删除该键
|
||
def _checkTTL(self):
|
||
nowTime = time()
|
||
for k in self._pressSet.copy():
|
||
if nowTime >= self._ttlDict[k]:
|
||
logger.debug(f"忽略超时按键 {k}")
|
||
del self._ttlDict[k]
|
||
self._pressSet.discard(k)
|
||
|
||
|
||
HotkeyCtrl = _HotkeyController()
|