Skip to content

Commit 1962c16

Browse files
HannahPaddloucass003
authored andcommitted
Keybinds mappings + Linux
1 parent 96cddb5 commit 1962c16

27 files changed

Lines changed: 1059 additions & 216 deletions

gui/public/i18n/en/translation.ftl

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,29 @@ settings-stay_aligned-debug-label = Debugging
605605
settings-stay_aligned-debug-description = Please include your settings when reporting problems about Stay Aligned.
606606
settings-stay_aligned-debug-copy-label = Copy settings to clipboard
607607
608+
settings-keybinds = Keybind settings
609+
settings-keybinds_ = ''
610+
settings-keybinds-description = Change keybinds for various shortcuts
611+
keybind_config-keybind_name = Keybind
612+
keybind_config-keybind_value = Combination
613+
keybind_config-keybind_delay = Delay before trigger (s)
614+
settings-keybinds_full-reset = Full Reset
615+
settings-keybinds_yaw-reset = Yaw Reset
616+
settings-keybinds_mounting-reset = Mounting Reset
617+
settings-keybinds_feet-mounting-reset = Feet Mounting Reset
618+
settings-keybinds_pause-tracking = Pause Tracking
619+
settings-keybinds_record-keybind = Click to record
620+
settings-keybinds_now-recording = Recording…
621+
settings-keybinds_reset-button = Reset
622+
settings-keybinds_reset-all-button = Reset all
623+
settings-keybinds-wayland-description = You appear to be using wayland, Please change your shortcuts in your system settings.
624+
settings-keybinds-wayland-open-system-settings-button = Open system settings
625+
settings-sidebar-keybinds = Keybinds
626+
settings-keybinds-recorder-modal-title = Assign keybind for
627+
settings-keybinds-recorder-modal-reset-button = Reset
628+
settings-keybinds-recorder-modal-unbind-button = Unbind
629+
settings-keybinds-recorder-modal-done-button = Done
630+
608631
## FK/Tracking settings
609632
settings-general-fk_settings = Tracking settings
610633

gui/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import { QuizSlimeSetQuestion } from './components/onboarding/pages/quiz/SlimeSe
5656
import { QuizUsageQuestion } from './components/onboarding/pages/quiz/UsageQuestion';
5757
import { QuizRuntimeQuestion } from './components/onboarding/pages/quiz/RuntimeQuestion';
5858
import { QuizMocapPosQuestion } from './components/onboarding/pages/quiz/MocapPreferencesQuestions';
59+
import { KeybindSettings } from './components/settings/pages/KeybindSettings';
5960
import { ElectronContextC, provideElectron } from './hooks/electron';
6061
import { AppLocalizationProvider } from './i18n/config';
6162
import { openUrl } from './hooks/crossplatform';
@@ -145,6 +146,7 @@ function Layout() {
145146
<Route path="interface" element={<InterfaceSettings />} />
146147
<Route path="interface/home" element={<HomeScreenSettings />} />
147148
<Route path="advanced" element={<AdvancedSettings />} />
149+
<Route path="keybinds" element={<KeybindSettings />} />
148150
</Route>
149151
<Route
150152
path="/onboarding"
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { useState, forwardRef, useRef } from 'react';
2+
import { Typography } from './Typography';
3+
import classNames from 'classnames';
4+
import { useFormContext } from 'react-hook-form';
5+
6+
const excludedKeys = [' ', 'SPACE', 'META'];
7+
const maxKeybindLength = 4;
8+
9+
export const KeybindRecorder = forwardRef<
10+
HTMLInputElement,
11+
{
12+
keys: string[];
13+
onKeysChange: (v: string[]) => void;
14+
error?: string;
15+
}
16+
>(function KeybindRecorder({ keys, onKeysChange, error }) {
17+
const [localKeys, setLocalKeys] = useState<string[]>(keys);
18+
const [isRecording, setIsRecording] = useState(false);
19+
const [oldKeys, setOldKeys] = useState<string[]>([]);
20+
const [invalidSlot, setInvalidSlot] = useState<number | null>(null);
21+
const [errorText, setErrorText] = useState<string>('');
22+
const inputRef = useRef<HTMLInputElement>(null);
23+
const displayKeys = isRecording ? localKeys : keys;
24+
const activeIndex = isRecording ? displayKeys.length : -1;
25+
const displayError = errorText || error;
26+
27+
const { clearErrors } = useFormContext();
28+
29+
const handleKeyDown = (e: React.KeyboardEvent) => {
30+
e.preventDefault();
31+
const key = e.key.toUpperCase();
32+
const errorMsg = excludedKeys.includes(key)
33+
? `Cannot use ${key}!`
34+
: displayKeys.includes(key)
35+
? `${key} is a Duplicate Key!`
36+
: null;
37+
if (errorMsg) {
38+
setErrorText(errorMsg);
39+
setInvalidSlot(activeIndex);
40+
setTimeout(() => {
41+
setInvalidSlot(null);
42+
}, 350);
43+
return;
44+
}
45+
46+
if (displayKeys.length < maxKeybindLength) {
47+
const updatedKeys = [...displayKeys, key];
48+
setLocalKeys(updatedKeys);
49+
onKeysChange(updatedKeys);
50+
if (updatedKeys.length == maxKeybindLength) {
51+
inputRef.current?.blur();
52+
}
53+
}
54+
};
55+
56+
const handleOnBlur = () => {
57+
setIsRecording(false);
58+
if (displayKeys.length < maxKeybindLength - 2 || error) {
59+
onKeysChange(oldKeys);
60+
setLocalKeys(oldKeys);
61+
}
62+
};
63+
64+
const handleOnFocus = () => {
65+
clearErrors('keybinds');
66+
const initialKeys: string[] = [];
67+
setOldKeys(keys);
68+
setLocalKeys(initialKeys);
69+
onKeysChange(initialKeys);
70+
setIsRecording(true);
71+
};
72+
73+
return (
74+
<div className="w-full justify-center items-center flex flex-col gap-2">
75+
<div className="flex gap-2 p-2 items-center rounded-lg relative">
76+
<input
77+
autoFocus
78+
ref={inputRef}
79+
className="opacity-0 absolute cursor-pointer w-full"
80+
onFocus={handleOnFocus}
81+
onBlur={handleOnBlur}
82+
onKeyDown={handleKeyDown}
83+
/>
84+
<div className="flex flex-grow gap-2 justify-center h-full">
85+
{Array.from({ length: maxKeybindLength }).map((_, i) => {
86+
const key = displayKeys[i];
87+
const isActive = isRecording && i === activeIndex;
88+
const isInvalid = invalidSlot === i;
89+
return (
90+
<div key={i} className="flex flex-row">
91+
<div
92+
className={classNames(
93+
'flex p-2 rounded-lg min-w-[50px] min-h-[50px] text-main-title justify-center items-center bg-background-80 mobile:text-sm',
94+
{
95+
'keyslot-invalid ring-2 ring-status-critical': isInvalid,
96+
'keyslot-animate ring-2 ring-accent':
97+
isActive && !isInvalid,
98+
'ring-accent': !isInvalid && !isInvalid,
99+
}
100+
)}
101+
>
102+
{key ?? ''}
103+
</div>
104+
<div className="flex pl-2 text-main-title justify-center items-center mobile:text-sm">
105+
{i < maxKeybindLength - 1 ? '+' : ''}
106+
</div>
107+
</div>
108+
);
109+
})}
110+
</div>
111+
</div>
112+
{displayError && (
113+
<div className="isInvalid keyslot-invalid">
114+
<Typography color="text-status-critical">{`${errorText} ${error}`}</Typography>
115+
</div>
116+
)}
117+
</div>
118+
);
119+
});
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { BaseModal } from './BaseModal';
2+
import { Controller, Control, useFormContext } from 'react-hook-form';
3+
import { KeybindRecorder } from './KeybindRecorder';
4+
import { Typography } from './Typography';
5+
import { Button } from './Button';
6+
import './KeybindRow.scss';
7+
import { useLocalization } from '@fluent/react';
8+
9+
export function KeybindRecorderModal({
10+
id,
11+
control,
12+
name,
13+
isVisisble,
14+
onClose,
15+
onUnbind,
16+
onSubmit,
17+
}: {
18+
id?: string;
19+
control: Control<any>;
20+
name: string;
21+
isVisisble: boolean;
22+
onClose: () => void;
23+
onUnbind: () => void;
24+
onSubmit: () => void;
25+
}) {
26+
const { l10n } = useLocalization();
27+
const keybindlocalization = 'settings-keybinds_' + id;
28+
const {
29+
formState: { errors },
30+
resetField,
31+
handleSubmit,
32+
} = useFormContext();
33+
34+
return (
35+
<BaseModal
36+
isOpen={isVisisble}
37+
onRequestClose={onClose}
38+
appendClasses="w-full max-w-xl"
39+
>
40+
<div className="flex flex-col gap-3 w-full justify-between h-full">
41+
<Typography variant="section-title">
42+
{l10n.getString('settings-keybinds-recorder-modal-title')}{' '}
43+
{l10n.getString(keybindlocalization)}
44+
</Typography>
45+
<Controller
46+
control={control}
47+
name={name}
48+
render={({ field }) => (
49+
<KeybindRecorder
50+
keys={field.value ?? []}
51+
onKeysChange={field.onChange}
52+
ref={field.ref}
53+
error={errors.keybinds?.message as string}
54+
/>
55+
)}
56+
/>
57+
<div className="flex flex-row justify-between w-full">
58+
<div className="flex flex-row justify-start gap-4">
59+
<Button
60+
id="settings-keybinds-recorder-modal-reset-button"
61+
variant="tertiary"
62+
onClick={() => {
63+
resetField(name);
64+
handleSubmit(onSubmit)();
65+
}}
66+
/>
67+
<Button
68+
id="settings-keybinds-recorder-modal-unbind-button"
69+
variant="tertiary"
70+
onClick={() => {
71+
onUnbind();
72+
handleSubmit(onSubmit)();
73+
}}
74+
/>
75+
</div>
76+
<div className="flex flex-row justify-end">
77+
<Button
78+
id="settings-keybinds-recorder-modal-done-button"
79+
variant="primary"
80+
onClick={handleSubmit(onSubmit)}
81+
/>
82+
</div>
83+
</div>
84+
</div>
85+
</BaseModal>
86+
);
87+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
.keybind-row {
2+
display: grid;
3+
grid-column: 1 / -1;
4+
grid-template-columns: subgrid;
5+
height: auto;
6+
align-items: center;
7+
gap: 10px;
8+
}
9+
10+
@keyframes keyslot {
11+
0%,
12+
100% {
13+
transform: scale(1);
14+
opacity: 0.6;
15+
}
16+
17+
50% {
18+
transform: scale(1.08);
19+
opacity: 1;
20+
}
21+
}
22+
23+
@keyframes shake {
24+
0% {
25+
transform: translate(1px, 1px) rotate(0deg);
26+
}
27+
10% {
28+
transform: translate(-1px, -2px) rotate(-1deg);
29+
}
30+
20% {
31+
transform: translate(-3px, 0px) rotate(1deg);
32+
}
33+
30% {
34+
transform: translate(3px, 2px) rotate(0deg);
35+
}
36+
40% {
37+
transform: translate(1px, -1px) rotate(1deg);
38+
}
39+
50% {
40+
transform: translate(-1px, 2px) rotate(-1deg);
41+
}
42+
60% {
43+
transform: translate(-3px, 1px) rotate(0deg);
44+
}
45+
70% {
46+
transform: translate(3px, 1px) rotate(-1deg);
47+
}
48+
80% {
49+
transform: translate(-1px, -1px) rotate(1deg);
50+
}
51+
90% {
52+
transform: translate(1px, 2px) rotate(0deg);
53+
}
54+
100% {
55+
transform: translate(1px, -2px) rotate(-1deg);
56+
}
57+
}
58+
59+
.keyslot-animate {
60+
animation: keyslot 1s ease-in-out infinite;
61+
}
62+
63+
.keyslot-invalid {
64+
animation: shake 0.35s ease;
65+
}

0 commit comments

Comments
 (0)