diff --git a/cspell.json b/cspell.json index e586926d8..d84b18001 100644 --- a/cspell.json +++ b/cspell.json @@ -36,6 +36,7 @@ "installdir", "Jsonifiable", "konami", + "keybind", "lezer", "LOCALAPPDATA", "logname", diff --git a/src/renderer/modules/components/KeybindItem.css b/src/renderer/modules/components/KeybindItem.css new file mode 100644 index 000000000..10bccfc02 --- /dev/null +++ b/src/renderer/modules/components/KeybindItem.css @@ -0,0 +1,90 @@ +.rp-keybind-container { + padding: 10px; + display: flex; + align-items: center; +} + +.rp-keybind-container:not(.recording) { + box-shadow: 0px 0px 2.5px #161616; +} + +.rp-keybind-container:hover:not(.recording) { + border: 0px solid #ffffff; + box-shadow: 0px 0px 2.5px #ffffff; +} + +.rp-keybind-container.recording > * { + border-color: #e73434; + box-shadow: 0px 0px 0.25px #e73434; +} + +.rp-keybind-container .text { + margin-right: 10px; +} + +.rp-keybind-container .text-truncate { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + max-height: 2em; +} + +.rp-keybind-container > .buttons { + display: flex; + justify-content: flex-end; + flex-grow: 1; +} + +.rp-keybind-container > .buttons .button { + height: 25px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + margin-right: 10px; + padding: 5px 10px; + border-radius: 5px; + opacity: 0.75; + cursor: pointer; +} + +.rp-keybind-container > .buttons .button.recording:hover { + background-color: #e7343414; +} + +.rp-keybind-container > .buttons .button:hover { + background-color: var(--background-secondary-alt); +} + +.rp-keybind-container > .buttons .button .icon { + display: inline-block; + margin-right: 5px; +} + +.rp-keybind-container > .buttons .button .button-text { + display: none; +} + +.rp-keybind-container > .buttons .button:hover .icon { + display: none; +} + +.rp-keybind-container > .buttons .button:hover .button-text { + display: inline-block; +} + +.rp-keybind-container.recording .text, +.rp-keybind-container.recording .button-text { + color: #e73434 !important; +} + +.rp-keybind-container > .buttons .button { + transition: padding 0.2s ease; +} + +.rp-keybind-container > .buttons .button:hover { + padding: 5px 15px; +} diff --git a/src/renderer/modules/components/KeybindItem.tsx b/src/renderer/modules/components/KeybindItem.tsx new file mode 100644 index 000000000..129263b9c --- /dev/null +++ b/src/renderer/modules/components/KeybindItem.tsx @@ -0,0 +1,295 @@ +import React from "@common/react"; + +import { FormItem, Text } from "."; +import { KeyboardEvent } from "electron"; +import "./KeybindItem.css"; + +type ValueArray = Array<{ + altKey: boolean | undefined; + code: string | undefined; + ctrlKey: boolean | undefined; + key: string | undefined; + keyCode: number | undefined; + metaKey: boolean | undefined; + shiftKey: boolean | undefined; +}>; + +interface ButtonsProps { + isRecording: boolean; + toggleRecording: () => void; + recordedKeybind: ValueArray; + clearKeybind: () => void; +} + +interface KeybindProps { + title?: string; + children?: string; + note?: string; + value: ValueArray; + placeholder?: string; + onChange: (newValue: ValueArray) => void; +} +interface KeybindState { + recordedKeybind: ValueArray; + currentlyPressed: number[]; + isRecording: boolean; + isEditing: boolean; + timer: null | NodeJS.Timer; +} + +interface ExtendedKeyboardEvent extends KeyboardEvent { + code: string | undefined; + key: string | undefined; + keyCode: number | undefined; +} +export type KeybindType = React.ComponentClass; + +const Buttons: React.FC = (props) => { + if (!props.recordedKeybind.length && !props.isRecording) { + return ( +
+
{ + event.stopPropagation(); + props.toggleRecording(); + }}> + + + + Record Keybind +
+
+ ); + } + + if (props.isRecording) { + return ( +
+
{ + event.stopPropagation(); + props.toggleRecording(); + }}> + + + + Stop Recording +
+
+ ); + } + + return ( +
+
{ + event.stopPropagation(); + props.toggleRecording(); + }}> + + + + Edit Keybind +
+
{ + event.stopPropagation(); + props.clearKeybind(); + }}> + + + + Clear Keybind +
+
+ ); +}; + +export default class KeybindRecorder extends React.Component { + public constructor(props: KeybindProps) { + super(props); + this.state = { + recordedKeybind: props.value ?? [], + currentlyPressed: [], + isRecording: false, + isEditing: false, + timer: null, + }; + } + + public resetTimer(): void { + clearTimeout(this.state.timer!); + } + + public stopRecording(): void { + this.setState({ + isRecording: false, + currentlyPressed: [], + }); + this.props.onChange(this.state.recordedKeybind); + } + + public startRecording(): void { + this.setState({ + isRecording: true, + recordedKeybind: [], + currentlyPressed: [], + timer: setTimeout(() => { + this.stopRecording(); + }, 3000), + }); + } + + public editRecording(): void { + this.setState({ + isRecording: true, + isEditing: true, + currentlyPressed: [], + timer: setTimeout(() => { + this.stopRecording(); + }, 3000), + }); + } + + public clearKeybind(): void { + this.setState({ + recordedKeybind: [], + currentlyPressed: [], + }); + this.props.onChange([]); + } + + public toggleRecording(): void { + if (this.state.isRecording) { + this.stopRecording(); + } else if (this.state.recordedKeybind.length) { + this.editRecording(); + } else { + this.startRecording(); + } + } + + public handleKeyDown(event: ExtendedKeyboardEvent): void { + this.resetTimer(); + const { isRecording, recordedKeybind, isEditing } = this.state; + if ( + isRecording && + event?.keyCode && + (!recordedKeybind.some((ck) => ck?.keyCode === event?.keyCode) || isEditing) + ) { + this.setState((prevState) => ({ + recordedKeybind: isEditing + ? [ + { + altKey: event.altKey, + code: event.code, + ctrlKey: event.ctrlKey, + key: event.key, + keyCode: event.keyCode, + metaKey: event.metaKey, + shiftKey: event.shiftKey, + }, + ] + : [ + ...prevState.recordedKeybind, + { + altKey: event.altKey, + code: event.code, + ctrlKey: event.ctrlKey, + key: event.key, + keyCode: event.keyCode, + metaKey: event.metaKey, + shiftKey: event.shiftKey, + }, + ], + currentlyPressed: [...prevState.currentlyPressed, event.keyCode!], + })); + if (isEditing) { + this.setState({ + isEditing: false, + }); + } + } + } + + public handleKeyUp(event: ExtendedKeyboardEvent): void { + const { isRecording } = this.state; + if (isRecording) { + this.setState((prevState) => ({ + currentlyPressed: prevState.currentlyPressed.filter((ps) => ps === event.keyCode), + })); + } + } + + public componentDidMount(): void { + document.addEventListener("keydown", this.handleKeyDown.bind(this)); + document.addEventListener("keyup", this.handleKeyUp.bind(this)); + } + + public componentWillUnmount(): void { + document.removeEventListener("keydown", this.handleKeyDown.bind(this)); + document.removeEventListener("keyup", this.handleKeyUp.bind(this)); + } + + public componentDidUpdate(_prevProps: KeybindProps, prevState: KeybindState): void { + const { isRecording, currentlyPressed } = this.state; + if (isRecording && currentlyPressed.length === 0 && prevState.currentlyPressed.length !== 0) { + this.stopRecording(); + } + } + + public render(): React.ReactNode { + const { recordedKeybind, isRecording } = this.state; + + return ( + +
+ + {recordedKeybind?.length + ? recordedKeybind + ?.map((rk) => + rk?.code?.toLowerCase()?.includes("right") + ? `RIGHT ${rk?.key?.toUpperCase()}` + : rk?.key?.toUpperCase(), + ) + .join(" + ") + : this.props.placeholder ?? "No Keybind Set"} + + +
+
+ ); + } +} diff --git a/src/renderer/modules/components/index.ts b/src/renderer/modules/components/index.ts index 2dd176d30..956a0f0af 100644 --- a/src/renderer/modules/components/index.ts +++ b/src/renderer/modules/components/index.ts @@ -158,6 +158,11 @@ export type { NoticeType }; export let Notice: NoticeType; importTimeout("Notice", import("./Notice"), (mod) => (Notice = mod.default)); +import type { KeybindType } from "./KeybindItem"; +export type { KeybindType }; +export let KeybindItem: KeybindType; +importTimeout("KeybindItem", import("./KeybindItem"), (mod) => (KeybindItem = mod.default)); + /** * @internal * @hidden