feat: add custom shortcut

This commit is contained in:
Guoguo
2025-12-04 17:54:30 +08:00
parent 699e132aad
commit 69d7e3fe96
32 changed files with 784 additions and 187 deletions

View File

@@ -87,7 +87,7 @@ export const Keyboard = () => {
// send keyboard data
async function sendKeyData(event: KeyboardEvent) {
const modifiers = getModifiers(event);
const modifiers = Modifiers.getModifiers(event, pressedModifiersRef.current);
const keys = [
0x00,
0x00,
@@ -98,32 +98,5 @@ export const Keyboard = () => {
await device.sendKeyboardData(modifiers, keys);
}
function getModifiers(event: KeyboardEvent) {
const modifiers = new Modifiers();
if (event.ctrlKey) {
modifiers.leftCtrl = pressedModifiersRef.current.has('ControlLeft');
modifiers.rightCtrl = pressedModifiersRef.current.has('ControlRight');
}
if (event.shiftKey) {
modifiers.leftShift = pressedModifiersRef.current.has('ShiftLeft');
modifiers.rightShift = pressedModifiersRef.current.has('ShiftRight');
}
if (event.altKey) {
modifiers.leftAlt = pressedModifiersRef.current.has('AltLeft');
modifiers.rightAlt = pressedModifiersRef.current.has('AltRight');
}
if (event.metaKey) {
modifiers.leftWindows = pressedModifiersRef.current.has('MetaLeft');
modifiers.rightWindows = pressedModifiersRef.current.has('MetaRight');
}
if (event.getModifierState('AltGraph')) {
modifiers.leftCtrl = true;
modifiers.rightAlt = true;
}
return modifiers;
}
return <></>;
};

View File

@@ -0,0 +1,159 @@
import { useEffect, useRef, useState } from 'react';
import { Input, InputRef, Modal, Space } from 'antd';
import { useSetAtom } from 'jotai';
import { KeyboardIcon, SquarePenIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { isKeyboardEnableAtom } from '@/jotai/keyboard.ts';
import { modifierKeys, Modifiers, ShortcutProps } from '@/libs/device/keyboard.ts';
interface KeyboardShortcutCustomProps {
addShortcut: (shortcut: ShortcutProps) => void;
}
export const KeyboardShortcutCustom = ({ addShortcut }: KeyboardShortcutCustomProps) => {
const { t } = useTranslation();
const setIsKeyboardEnable = useSetAtom(isKeyboardEnableAtom);
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const [isCapturingShortcut, setIsCapturingShortcut] = useState<boolean>(false);
const [shortcut, setShortcut] = useState<ShortcutProps | null>(null);
const [shortcutLabel, setShortcutLabel] = useState<string>('');
const [shortcutFixedLabel, setShortcutFixedLabel] = useState<string>('');
const pressedModifiersRef = useRef<Set<string>>(new Set());
const inputRef = useRef<InputRef>(null);
function getPressedShortcutString(event: KeyboardEvent) {
let pressedKey: string = '';
pressedKey += event.ctrlKey ? 'Ctrl + ' : '';
pressedKey += event.shiftKey ? 'Shift + ' : '';
pressedKey += event.altKey ? 'Alt + ' : '';
pressedKey += event.metaKey ? 'Meta + ' : '';
if (!modifierKeys.has(event.key)) {
if (event.code.startsWith('Key') || event.code.startsWith('Digit')) {
pressedKey += event.code.slice(-1);
} else {
pressedKey += event.key;
}
}
return pressedKey;
}
function handleModelDone() {
if (shortcut == null) {
cleanShortcut();
return;
}
addShortcut({
modifiers: shortcut.modifiers,
label: shortcutLabel == '' ? shortcutFixedLabel : shortcutLabel,
keyCode: shortcut.keyCode
});
cleanShortcut();
}
function cleanShortcut() {
setIsCapturingShortcut(false);
setIsModalOpen(false);
setShortcut(null);
setShortcutLabel('');
setShortcutFixedLabel('');
}
async function handleKeyDown(event: KeyboardEvent) {
event.preventDefault();
event.stopPropagation();
const label = getPressedShortcutString(event);
setShortcutFixedLabel(label);
if (modifierKeys.has(event.key)) {
if (event.type == 'keydown') {
pressedModifiersRef.current.add(event.code);
} else {
pressedModifiersRef.current.delete(event.code);
}
} else {
setIsCapturingShortcut(false);
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyDown);
const modifiers = Modifiers.getModifiers(event, pressedModifiersRef.current);
const ret: ShortcutProps = {
modifiers: modifiers,
label: label,
keyCode: event.code
};
setShortcut(ret);
}
}
function handleFullscreen() {
if (!document.fullscreenElement) {
const element = document.documentElement;
element.requestFullscreen().then();
// @ts-expect-error - https://developer.mozilla.org/en-US/docs/Web/API/Keyboard/lock
navigator.keyboard?.lock();
}
}
useEffect(() => {
setIsKeyboardEnable(!isModalOpen);
}, [isModalOpen]);
useEffect(() => {
if (inputRef.current && !isCapturingShortcut) {
inputRef.current.blur();
}
}, [isCapturingShortcut]);
return (
<>
<div
className="flex h-[30px] cursor-pointer items-center space-x-1 rounded px-3 text-neutral-300 hover:bg-neutral-700/60"
onClick={() => {
setIsModalOpen(true);
}}
>
<span>{t('keyboard.shortcut.custom')}</span>
</div>
<Modal
width={400}
title={t('keyboard.shortcut.custom')}
open={isModalOpen}
onOk={handleModelDone}
onCancel={cleanShortcut}
okText={t('keyboard.shortcut.save')}
cancelText={t('keyboard.shortcut.cancel')}>
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<div className="flex">
{t('keyboard.shortcut.captureTips')}
<a onClick={handleFullscreen}>{t('keyboard.shortcut.enterFullScreen')}</a>
</div>
<Input
ref={inputRef}
placeholder={t('keyboard.shortcut.capture')}
value={shortcutFixedLabel}
prefix={<KeyboardIcon size={16} />}
onFocus={() => {
setIsCapturingShortcut(true);
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyDown);
}}
onBlur={() => {
setIsCapturingShortcut(false);
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyDown);
}}
/>
<Input
placeholder={shortcutFixedLabel || t('keyboard.shortcut.label')}
value={shortcutLabel}
prefix={<SquarePenIcon size={16} />}
onChange={(e) => setShortcutLabel(e.target.value)}
/>
</Space>
</Modal>
</>
);
};

View File

@@ -1,17 +1,12 @@
import { useState } from 'react';
import { SendHorizonal } from 'lucide-react';
import { useState, PropsWithChildren } from 'react';
import { device } from '@/libs/device';
import { Modifiers } from '@/libs/device/keyboard.ts';
import { KeyboardCodes } from '@/libs/keyboard';
import { ShortcutProps } from '@/libs/device/keyboard.ts'
interface ShortcutProps {
label: string;
modifiers?: Partial<Modifiers>;
keyCode: string;
}
type ShortcutPropsWithChildren = ShortcutProps & PropsWithChildren
export const Shortcut = ({ label, modifiers = {}, keyCode }: ShortcutProps) => {
export const Shortcut = ({ label, modifiers = {}, keyCode , children}: ShortcutPropsWithChildren) => {
const [isLoading, setIsLoading] = useState(false);
async function handleClick(): Promise<void> {
@@ -40,8 +35,8 @@ export const Shortcut = ({ label, modifiers = {}, keyCode }: ShortcutProps) => {
className="flex h-[30px] cursor-pointer items-center space-x-1 rounded px-3 text-neutral-300 hover:bg-neutral-700/60"
onClick={handleClick}
>
<SendHorizonal size={18} />
<span>{label}</span>
<span className="w-full">{label}</span>
{children}
</div>
);
};

View File

@@ -1,42 +1,92 @@
import { useState } from 'react';
import { SendHorizonal } from 'lucide-react';
import { useEffect, useState } from 'react';
import { Button, Divider, Popover } from 'antd';
import { SendHorizonal, Trash } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Popover } from 'antd';
import { ShortcutProps } from '@/libs/device/keyboard.ts';
import { Shortcut } from './shortcut.tsx';
import { KeyboardShortcutCustom } from './shortcut-custom.tsx';
export const KeyboardShortcutsMenu = () => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [storedShortcuts, setStoredShortcuts] = useState<ShortcutProps[]>([]);
const predefinedShortcuts : ShortcutProps[] = [
{
label: t('keyboard.shortcut.ctrlAltDel'),
modifiers: { leftCtrl: true, leftAlt: true },
keyCode: 'Delete',
},
{
label: t('keyboard.shortcut.ctrlD'),
modifiers: { leftCtrl: true },
keyCode: 'KeyD',
},
{
label: t('keyboard.shortcut.winTab'),
modifiers: { leftWindows: true },
keyCode: 'Tab',
},
]
const addShortcut = (shortcut: ShortcutProps) => {
if (shortcut == null) return;
let shortcuts = localStorage.getItem("shortcuts");
if (shortcuts === null) {
shortcuts = "[]";
}
const shortcutsObject: ShortcutProps[] = JSON.parse(shortcuts);
shortcutsObject.push(shortcut);
localStorage.setItem("shortcuts", JSON.stringify(shortcutsObject));
setStoredShortcuts(shortcutsObject);
}
const removeShortcut = (indexToRemove: number) => {
const newShortcuts = storedShortcuts.filter(
(_, index) => index !== indexToRemove
);
localStorage.setItem("shortcuts", JSON.stringify(newShortcuts));
setStoredShortcuts(newShortcuts);
}
useEffect(() => {
const shortcuts = localStorage.getItem("shortcuts");
if (shortcuts === null) {
predefinedShortcuts.forEach(shortcut => {
addShortcut(shortcut);
})
} else {
const shortcutsObject: ShortcutProps[] = JSON.parse(shortcuts);
setStoredShortcuts(shortcutsObject);
}
}, []);
return (
<Popover
content={
<div className="flex flex-col gap-1">
{[
{
label: t('keyboard.ctrlAltDel'),
modifiers: { leftCtrl: true, leftAlt: true },
keyCode: 'Delete',
},
{
label: t('keyboard.ctrlD'),
modifiers: { leftCtrl: true },
keyCode: 'KeyD',
},
{
label: t('keyboard.winTab'),
modifiers: { leftWindows: true },
keyCode: 'Tab',
},
].map((shortcut) => (
<Shortcut
key={shortcut.keyCode}
label={shortcut.label}
modifiers={shortcut.modifiers}
keyCode={shortcut.keyCode}
/>
{storedShortcuts.map((shortcut, index) => (
<>
<Shortcut
key={index}
label={shortcut.label}
modifiers={shortcut.modifiers}
keyCode={shortcut.keyCode}
>
<Button
className="ml-auto"
type="text"
danger
icon={<Trash size={16} />}
onClick={() => {removeShortcut(index)}}
/>
</Shortcut>
</>
))}
<Divider style={{ margin: '5px 0 5px 0' }} />
<KeyboardShortcutCustom
addShortcut={addShortcut}/>
</div>
}
trigger="click"
@@ -48,7 +98,7 @@ export const KeyboardShortcutsMenu = () => {
>
<div className="flex h-[30px] cursor-pointer items-center space-x-1 rounded px-3 text-neutral-300 hover:bg-neutral-700/60">
<SendHorizonal size={18} />
<span>{t('keyboard.shortcuts')}</span>
<span>{t('keyboard.shortcut.title')}</span>
</div>
</Popover>
);

View File

@@ -38,10 +38,12 @@ const be = {
keyboard: {
paste: 'Plakken',
virtualKeyboard: 'Virtueel klavier',
shortcuts: 'Sneltoetsen',
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
shortcut: {
title: 'Sneltoetsen',
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
},
},
mouse: {
cursor: {

View File

@@ -38,10 +38,12 @@ const de = {
keyboard: {
paste: 'Einfügen',
virtualKeyboard: 'Virtuelle Tastatur',
shortcuts: 'Tastenkürzel',
ctrlAltDel: 'Strg + Alt + Entfernen',
ctrlD: 'Strg + D',
winTab: 'Win + Tab',
shortcut: {
title: 'Tastenkürzel',
ctrlAltDel: 'Strg + Alt + Entfernen',
ctrlD: 'Strg + D',
winTab: 'Win + Tab',
},
},
mouse: {
cursor: {

View File

@@ -38,10 +38,19 @@ const en = {
keyboard: {
paste: 'Paste',
virtualKeyboard: 'Keyboard',
shortcuts: 'Shortcuts',
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
shortcut: {
title: 'Shortcut',
custom: 'Custom Shortcut',
capture: 'Click here to capture shortcut',
label: 'Label',
cancel: 'Cancel',
save: 'Save',
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
enterFullScreen: 'Enter Full Screen',
captureTips: 'Capturing the Meta key separately requires operation in full-screen mode.'
},
},
mouse: {
cursor: {

View File

@@ -38,7 +38,11 @@ const ko = {
keyboard: {
paste: '붙여넣기',
virtualKeyboard: '가상 키보드',
ctrlAltDel: 'Ctrl + Alt + Delete'
shortcut: {
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
},
},
mouse: {
cursor: {

View File

@@ -38,10 +38,12 @@ const nl = {
keyboard: {
paste: 'Plakken',
virtualKeyboard: 'Virtueel toetsenbord',
shortcuts: 'Sneltoetsen',
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
shortcut: {
title: 'Sneltoetsen',
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
},
},
mouse: {
cursor: {

View File

@@ -37,7 +37,11 @@ const pl = {
keyboard: {
paste: 'Wklej',
virtualKeyboard: 'Klawiatura',
ctrlAltDel: 'Ctrl + Alt + Delete'
shortcut: {
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
},
},
mouse: {
cursor: {

View File

@@ -37,7 +37,11 @@ const pt_br = {
keyboard: {
paste: 'Colar',
virtualKeyboard: 'Teclado Virtual',
ctrlAltDel: 'Ctrl + Alt + Delete'
shortcut: {
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
},
},
mouse: {
cursor: {

View File

@@ -38,10 +38,12 @@ const ru = {
keyboard: {
paste: 'Вставить текст',
virtualKeyboard: 'Виртуальная клавиатура',
shortcuts: 'Сочетания клавиш',
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
shortcut: {
title: 'Сочетания клавиш',
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
},
},
mouse: {
cursor: {

View File

@@ -36,10 +36,19 @@ const zh = {
keyboard: {
paste: '粘贴',
virtualKeyboard: '虚拟键盘',
shortcuts: '快捷键',
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
shortcut: {
title: '快捷键',
custom: '自定义快捷键',
capture: '点击此处捕获快捷键',
label: '备注',
cancel: '取消',
save: '保存',
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
enterFullScreen: '进入全屏',
captureTips: '单独捕获 Meta 键需要在全屏下操作,'
},
},
mouse: {
cursor: {

View File

@@ -35,7 +35,11 @@ const zh_tw = {
keyboard: {
paste: '貼上',
virtualKeyboard: '虛擬鍵盤',
ctrlAltDel: 'Ctrl + Alt + Delete'
shortcut: {
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
},
},
mouse: {
cursor: {

View File

@@ -1,6 +1,14 @@
import { setBit } from './utils.ts';
export class Modifiers {
interface ShortcutProps {
label: string;
modifiers: Partial<Modifiers>;
keyCode: string;
}
const modifierKeys = new Set(['Control', 'Shift', 'Alt', 'Meta']);
class Modifiers {
public rightWindows: boolean = false;
public rightAlt: boolean = false;
public rightShift: boolean = false;
@@ -22,4 +30,32 @@ export class Modifiers {
b = setBit(b, 7, this.rightWindows);
return b;
}
public static getModifiers(event: KeyboardEvent, pressedModifiers: Set<string>) {
const modifiers = new Modifiers();
if (event.ctrlKey) {
modifiers.leftCtrl = pressedModifiers.has('ControlLeft');
modifiers.rightCtrl = pressedModifiers.has('ControlRight');
}
if (event.shiftKey) {
modifiers.leftShift = pressedModifiers.has('ShiftLeft');
modifiers.rightShift = pressedModifiers.has('ShiftRight');
}
if (event.altKey) {
modifiers.leftAlt = pressedModifiers.has('AltLeft');
modifiers.rightAlt = pressedModifiers.has('AltRight');
}
if (event.metaKey) {
modifiers.leftWindows = pressedModifiers.has('MetaLeft');
modifiers.rightWindows = pressedModifiers.has('MetaRight');
}
if (event.getModifierState('AltGraph')) {
modifiers.leftCtrl = true;
modifiers.rightAlt = true;
}
return modifiers;
}
}
export { type ShortcutProps, Modifiers, modifierKeys };

View File

@@ -1,5 +1,5 @@
import { ReactElement, useEffect, useRef } from 'react'
import { Modifiers } from '@renderer/libs/device/keyboard';
import { Modifiers } from '@renderer/libs/device/keyboard'
import { KeyboardCodes } from '@renderer/libs/keyboard'
import { IpcEvents } from '@common/ipc-events'
@@ -59,7 +59,7 @@ export const Keyboard = (): ReactElement => {
}
async function sendKeyData(event: KeyboardEvent) {
const modifiers = getModifiers(event)
const modifiers = Modifiers.getModifiers(event, pressedModifiersRef.current)
const keys = [
0x00,
0x00,
@@ -70,33 +70,6 @@ export const Keyboard = (): ReactElement => {
await window.electron.ipcRenderer.invoke(IpcEvents.SEND_KEYBOARD, modifiers.encode(), keys)
}
function getModifiers(event: KeyboardEvent) {
const modifiers = new Modifiers()
if (event.ctrlKey) {
modifiers.leftCtrl = pressedModifiersRef.current.has('ControlLeft')
modifiers.rightCtrl = pressedModifiersRef.current.has('ControlRight')
}
if (event.shiftKey) {
modifiers.leftShift = pressedModifiersRef.current.has('ShiftLeft')
modifiers.rightShift = pressedModifiersRef.current.has('ShiftRight')
}
if (event.altKey) {
modifiers.leftAlt = pressedModifiersRef.current.has('AltLeft')
modifiers.rightAlt = pressedModifiersRef.current.has('AltRight')
}
if (event.metaKey) {
modifiers.leftWindows = pressedModifiersRef.current.has('MetaLeft')
modifiers.rightWindows = pressedModifiersRef.current.has('MetaRight')
}
if (event.getModifierState('AltGraph')) {
modifiers.leftCtrl = true
modifiers.rightAlt = true
}
return modifiers
}
return <></>
}

View File

@@ -1,40 +0,0 @@
import { ReactElement, useState } from 'react'
import { SendHorizonal } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { IpcEvents } from '@common/ipc-events'
import { KeyboardCodes } from '@renderer/libs/keyboard'
export const CtrlAltDel = (): ReactElement => {
const { t } = useTranslation()
const [isLoading, setIsLoading] = useState(false)
async function ctrlAltDel(): Promise<void> {
if (isLoading) return
setIsLoading(true)
try {
await send(5, KeyboardCodes.get('Delete')!) // modifier 5 is ctrlLeft + altLeft
await send(0, 0)
} catch (e) {
console.log(e)
} finally {
setIsLoading(false)
}
}
async function send(modifier: number, key: number): Promise<void> {
await window.electron.ipcRenderer.invoke(IpcEvents.SEND_KEYBOARD, modifier, key)
}
return (
<div
className="flex h-[30px] cursor-pointer items-center space-x-1 rounded px-3 text-neutral-300 hover:bg-neutral-700/60"
onClick={ctrlAltDel}
>
<SendHorizonal size={18} />
<span>{t('keyboard.ctrlAltDel')}</span>
</div>
)
}

View File

@@ -1,19 +1,19 @@
import { ReactElement, useState } from 'react'
import { Popover } from 'antd'
import { KeyboardIcon } from 'lucide-react'
import { useState, ReactElement } from 'react';
import { Popover } from 'antd';
import { KeyboardIcon } from 'lucide-react';
import { CtrlAltDel } from './ctrl-alt-del'
import { Paste } from './paste'
import { VirtualKeyboard } from './virtual-keyboard'
import { Paste } from './paste';
import { VirtualKeyboard } from './virtual-keyboard';
import { KeyboardShortcutsMenu } from './shortcuts-menu';
export const Keyboard = (): ReactElement => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const content = (
<>
<Paste />
<CtrlAltDel />
<VirtualKeyboard />
<KeyboardShortcutsMenu />
</>
)
@@ -26,7 +26,7 @@ export const Keyboard = (): ReactElement => {
open={isPopoverOpen}
onOpenChange={setIsPopoverOpen}
>
<div className="flex h-[28px] cursor-pointer items-center justify-center rounded px-2 text-white hover:bg-neutral-700/70">
<div className="flex h-[28px] w-[28px] cursor-pointer items-center justify-center rounded text-white hover:bg-neutral-700/70">
<KeyboardIcon size={18} />
</div>
</Popover>

View File

@@ -0,0 +1,159 @@
import { useEffect, useRef, useState } from 'react';
import { Input, InputRef, Modal, Space } from 'antd';
import { KeyboardIcon, SquarePenIcon } from 'lucide-react';
import { isKeyboardEnableAtom } from '@renderer/jotai/keyboard';
import { modifierKeys, Modifiers, ShortcutProps } from '@renderer/libs/device/keyboard';
import { useSetAtom } from 'jotai'
import { useTranslation } from 'react-i18next'
interface KeyboardShortcutCustomProps {
addShortcut: (shortcut: ShortcutProps) => void;
}
export const KeyboardShortcutCustom = ({ addShortcut }: KeyboardShortcutCustomProps) => {
const { t } = useTranslation();
const setIsKeyboardEnable = useSetAtom(isKeyboardEnableAtom);
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const [isCapturingShortcut, setIsCapturingShortcut] = useState<boolean>(false);
const [shortcut, setShortcut] = useState<ShortcutProps | null>(null);
const [shortcutLabel, setShortcutLabel] = useState<string>('');
const [shortcutFixedLabel, setShortcutFixedLabel] = useState<string>('');
const pressedModifiersRef = useRef<Set<string>>(new Set());
const inputRef = useRef<InputRef>(null);
function getPressedShortcutString(event: KeyboardEvent) {
let pressedKey: string = '';
pressedKey += event.ctrlKey ? 'Ctrl + ' : '';
pressedKey += event.shiftKey ? 'Shift + ' : '';
pressedKey += event.altKey ? 'Alt + ' : '';
pressedKey += event.metaKey ? 'Meta + ' : '';
if (!modifierKeys.has(event.key)) {
if (event.code.startsWith('Key') || event.code.startsWith('Digit')) {
pressedKey += event.code.slice(-1);
} else {
pressedKey += event.key;
}
}
return pressedKey;
}
function handleModelDone() {
if (shortcut == null) {
cleanShortcut();
return;
}
addShortcut({
modifiers: shortcut.modifiers,
label: shortcutLabel == '' ? shortcutFixedLabel : shortcutLabel,
keyCode: shortcut.keyCode
});
cleanShortcut();
}
function cleanShortcut() {
setIsCapturingShortcut(false);
setIsModalOpen(false);
setShortcut(null);
setShortcutLabel('');
setShortcutFixedLabel('');
}
async function handleKeyDown(event: KeyboardEvent) {
event.preventDefault();
event.stopPropagation();
const label = getPressedShortcutString(event);
setShortcutFixedLabel(label);
if (modifierKeys.has(event.key)) {
if (event.type == 'keydown') {
pressedModifiersRef.current.add(event.code);
} else {
pressedModifiersRef.current.delete(event.code);
}
} else {
setIsCapturingShortcut(false);
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyDown);
const modifiers = Modifiers.getModifiers(event, pressedModifiersRef.current);
const ret: ShortcutProps = {
modifiers: modifiers,
label: label,
keyCode: event.code
};
setShortcut(ret);
}
}
function handleFullscreen() {
if (!document.fullscreenElement) {
const element = document.documentElement;
element.requestFullscreen().then();
// @ts-expect-error - https://developer.mozilla.org/en-US/docs/Web/API/Keyboard/lock
navigator.keyboard?.lock();
}
}
useEffect(() => {
setIsKeyboardEnable(!isModalOpen);
}, [isModalOpen]);
useEffect(() => {
if (inputRef.current && !isCapturingShortcut) {
inputRef.current.blur();
}
}, [isCapturingShortcut]);
return (
<>
<div
className="flex h-[30px] cursor-pointer items-center space-x-1 rounded px-3 text-neutral-300 hover:bg-neutral-700/60"
onClick={() => {
setIsModalOpen(true);
}}
>
<span>{t('keyboard.shortcut.custom')}</span>
</div>
<Modal
width={400}
title={t('keyboard.shortcut.custom')}
open={isModalOpen}
onOk={handleModelDone}
onCancel={cleanShortcut}
okText={t('keyboard.shortcut.save')}
cancelText={t('keyboard.shortcut.cancel')}>
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<div className="flex">
{t('keyboard.shortcut.captureTips')}
<a onClick={handleFullscreen}>{t('keyboard.shortcut.enterFullScreen')}</a>
</div>
<Input
ref={inputRef}
placeholder={t('keyboard.shortcut.capture')}
value={shortcutFixedLabel}
prefix={<KeyboardIcon size={16} />}
onFocus={() => {
setIsCapturingShortcut(true);
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyDown);
}}
onBlur={() => {
setIsCapturingShortcut(false);
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyDown);
}}
/>
<Input
placeholder={shortcutFixedLabel || t('keyboard.shortcut.label')}
value={shortcutLabel}
prefix={<SquarePenIcon size={16} />}
onChange={(e) => setShortcutLabel(e.target.value)}
/>
</Space>
</Modal>
</>
);
};

View File

@@ -0,0 +1,49 @@
import { useState, PropsWithChildren } from 'react';
import { SendHorizonal } from 'lucide-react';
import { Modifiers, ShortcutProps } from '@renderer/libs/device/keyboard';
import { KeyboardCodes } from '@renderer/libs/keyboard'
import { IpcEvents } from '@common/ipc-events'
type ShortcutPropsWithChildren = ShortcutProps & PropsWithChildren
export const Shortcut = (
{ label, modifiers = {}, keyCode , children}: ShortcutPropsWithChildren
) => {
const [isLoading, setIsLoading] = useState(false);
async function handleClick(): Promise<void> {
if (isLoading) return;
setIsLoading(true);
try {
const mods = new Modifiers();
Object.assign(mods, modifiers);
await send(mods, KeyboardCodes.get(keyCode)!);
} catch (e) {
console.log(e);
} finally {
setIsLoading(false);
}
}
async function send(mods: Modifiers, code: number) {
const keys = [0x00, 0x00, code, 0x00, 0x00, 0x00];
await window.electron.ipcRenderer.invoke(IpcEvents.SEND_KEYBOARD, mods.encode(), keys)
await window.electron.ipcRenderer.invoke(
IpcEvents.SEND_KEYBOARD,
new Modifiers().encode(),
[0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
);
}
return (
<div
className="flex h-[30px] cursor-pointer items-center space-x-1 rounded px-3 text-neutral-300 hover:bg-neutral-700/60"
onClick={handleClick}
>
<SendHorizonal size={18} />
<span className="w-full">{label}</span>
{children}
</div>
);
};

View File

@@ -0,0 +1,105 @@
import { useEffect, useState } from 'react';
import { Button, Divider, Popover } from 'antd';
import { SendHorizonal, Trash } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { ShortcutProps } from '@renderer/libs/device/keyboard';
import { Shortcut } from './shortcut';
import { KeyboardShortcutCustom } from './shortcut-custom';
export const KeyboardShortcutsMenu = () => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [storedShortcuts, setStoredShortcuts] = useState<ShortcutProps[]>([]);
const predefinedShortcuts : ShortcutProps[] = [
{
label: t('keyboard.shortcut.ctrlAltDel'),
modifiers: { leftCtrl: true, leftAlt: true },
keyCode: 'Delete',
},
{
label: t('keyboard.shortcut.ctrlD'),
modifiers: { leftCtrl: true },
keyCode: 'KeyD',
},
{
label: t('keyboard.shortcut.winTab'),
modifiers: { leftWindows: true },
keyCode: 'Tab',
},
]
const addShortcut = (shortcut: ShortcutProps) => {
if (shortcut == null) return;
let shortcuts = localStorage.getItem("shortcuts");
if (shortcuts === null) {
shortcuts = "[]";
}
const shortcutsObject: ShortcutProps[] = JSON.parse(shortcuts);
shortcutsObject.push(shortcut);
localStorage.setItem("shortcuts", JSON.stringify(shortcutsObject));
setStoredShortcuts(shortcutsObject);
}
const removeShortcut = (indexToRemove: number) => {
const newShortcuts = storedShortcuts.filter(
(_, index) => index !== indexToRemove
);
localStorage.setItem("shortcuts", JSON.stringify(newShortcuts));
setStoredShortcuts(newShortcuts);
}
useEffect(() => {
const shortcuts = localStorage.getItem("shortcuts");
if (shortcuts === null) {
predefinedShortcuts.forEach(shortcut => {
addShortcut(shortcut);
})
} else {
const shortcutsObject: ShortcutProps[] = JSON.parse(shortcuts);
setStoredShortcuts(shortcutsObject);
}
}, []);
return (
<Popover
content={
<div className="flex flex-col gap-1">
{storedShortcuts.map((shortcut, index) => (
<>
<Shortcut
key={index}
label={shortcut.label}
modifiers={shortcut.modifiers}
keyCode={shortcut.keyCode}
>
<Button
className="ml-auto"
type="text"
danger
icon={<Trash size={16} />}
onClick={() => {removeShortcut(index)}}
/>
</Shortcut>
</>
))}
<Divider style={{ margin: '5px 0 5px 0' }} />
<KeyboardShortcutCustom
addShortcut={addShortcut}/>
</div>
}
trigger="click"
placement="rightTop"
align={{ offset: [14, 0] }}
open={open}
onOpenChange={setOpen}
arrow={false}
>
<div className="flex h-[30px] cursor-pointer items-center space-x-1 rounded px-3 text-neutral-300 hover:bg-neutral-700/60">
<SendHorizonal size={18} />
<span>{t('keyboard.shortcut.title')}</span>
</div>
</Popover>
);
};

View File

@@ -40,7 +40,12 @@ const be = {
keyboard: {
paste: 'Plakken',
virtualKeyboard: 'Virtueel klavier',
ctrlAltDel: 'Ctrl + Alt + Delete'
shortcut: {
title: 'Sneltoetsen',
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
},
},
mouse: {
cursor: {

View File

@@ -40,7 +40,12 @@ const de = {
keyboard: {
paste: 'Einfügen',
virtualKeyboard: 'Virtuelle Tastatur',
ctrlAltDel: 'Strg + Alt + Entfernen'
shortcut: {
title: 'Tastenkürzel',
ctrlAltDel: 'Strg + Alt + Entfernen',
ctrlD: 'Strg + D',
winTab: 'Win + Tab',
},
},
mouse: {
cursor: {

View File

@@ -40,7 +40,19 @@ const en = {
keyboard: {
paste: 'Paste',
virtualKeyboard: 'Keyboard',
ctrlAltDel: 'Ctrl + Alt + Delete'
shortcut: {
title: 'Shortcut',
custom: 'Custom Shortcut',
capture: 'Click here to capture shortcut',
label: 'Label',
cancel: 'Cancel',
save: 'Save',
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
enterFullScreen: 'Enter Full Screen',
captureTips: 'Capturing the Meta key separately requires operation in full-screen mode.'
},
},
mouse: {
cursor: {

View File

@@ -30,10 +30,14 @@ const ko = {
cancel: '취소'
}
},
keyboard: {
kekeyboard: {
paste: '붙여넣기',
virtualKeyboard: '가상 키보드',
ctrlAltDel: 'Ctrl + Alt + Delete'
shortcut: {
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
},
},
mouse: {
cursor: {

View File

@@ -40,7 +40,12 @@ const nl = {
keyboard: {
paste: 'Plakken',
virtualKeyboard: 'Virtueel toetsenbord',
ctrlAltDel: 'Ctrl + Alt + Delete'
shortcut: {
title: 'Sneltoetsen',
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
},
},
mouse: {
cursor: {

View File

@@ -32,7 +32,11 @@ const pl = {
keyboard: {
paste: 'Wklej',
virtualKeyboard: 'Klawiatura',
ctrlAltDel: 'Ctrl + Alt + Delete'
shortcut: {
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
},
},
mouse: {
cursor: {

View File

@@ -32,7 +32,11 @@ const pt_br = {
keyboard: {
paste: 'Colar',
virtualKeyboard: 'Teclado Virtual',
ctrlAltDel: 'Ctrl + Alt + Delete'
shortcut: {
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
},
},
mouse: {
cursor: {

View File

@@ -40,7 +40,12 @@ const ru = {
keyboard: {
paste: 'Вставить текст',
virtualKeyboard: 'Виртуальная клавиатура',
ctrlAltDel: 'Ctrl + Alt + Delete'
shortcut: {
title: 'Сочетания клавиш',
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
},
},
mouse: {
cursor: {

View File

@@ -36,10 +36,22 @@ const zh = {
cancel: '取消'
}
},
keyboard: {
kkeyboard: {
paste: '粘贴',
virtualKeyboard: '虚拟键盘',
ctrlAltDel: 'Ctrl + Alt + Delete'
shortcut: {
title: '快捷键',
custom: '自定义快捷键',
capture: '点击此处捕获快捷键',
label: '备注',
cancel: '取消',
save: '保存',
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
enterFullScreen: '进入全屏',
captureTips: '单独捕获 Meta 键需要在全屏下操作,'
},
},
mouse: {
cursor: {

View File

@@ -35,7 +35,11 @@ const zh_tw = {
keyboard: {
paste: '貼上',
virtualKeyboard: '虛擬鍵盤',
ctrlAltDel: 'Ctrl + Alt + Delete'
shortcut: {
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
},
},
mouse: {
cursor: {

View File

@@ -1,6 +1,14 @@
import { setBit } from './utils';
export class Modifiers {
interface ShortcutProps {
label: string;
modifiers: Partial<Modifiers>;
keyCode: string;
}
const modifierKeys = new Set(['Control', 'Shift', 'Alt', 'Meta']);
class Modifiers {
public rightWindows: boolean = false;
public rightAlt: boolean = false;
public rightShift: boolean = false;
@@ -22,4 +30,32 @@ export class Modifiers {
b = setBit(b, 7, this.rightWindows);
return b;
}
public static getModifiers(event: KeyboardEvent, pressedModifiers: Set<string>) {
const modifiers = new Modifiers();
if (event.ctrlKey) {
modifiers.leftCtrl = pressedModifiers.has('ControlLeft');
modifiers.rightCtrl = pressedModifiers.has('ControlRight');
}
if (event.shiftKey) {
modifiers.leftShift = pressedModifiers.has('ShiftLeft');
modifiers.rightShift = pressedModifiers.has('ShiftRight');
}
if (event.altKey) {
modifiers.leftAlt = pressedModifiers.has('AltLeft');
modifiers.rightAlt = pressedModifiers.has('AltRight');
}
if (event.metaKey) {
modifiers.leftWindows = pressedModifiers.has('MetaLeft');
modifiers.rightWindows = pressedModifiers.has('MetaRight');
}
if (event.getModifierState('AltGraph')) {
modifiers.leftCtrl = true;
modifiers.rightAlt = true;
}
return modifiers;
}
}
export { type ShortcutProps, Modifiers, modifierKeys };