mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
I forgor to stage files
This commit is contained in:
55
gui/src/components/commons/KeybindRecorder.tsx
Normal file
55
gui/src/components/commons/KeybindRecorder.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import { useState, forwardRef } from 'react';
|
||||
|
||||
export const KeybindRecorder = forwardRef<
|
||||
HTMLInputElement,
|
||||
{
|
||||
keys: string[];
|
||||
onKeysChange: (v: string[]) => void;
|
||||
}
|
||||
>(function KeybindRecorder({ keys, onKeysChange }, ref) {
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [oldKeys, setOldKeys] = useState<string[]>([]);
|
||||
const { l10n } = useLocalization();
|
||||
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
<input
|
||||
ref={ref}
|
||||
className="opacity-0 absolute inset-0 cursor-pointer"
|
||||
onFocus={() => {
|
||||
setOldKeys(keys);
|
||||
onKeysChange([]);
|
||||
setIsRecording(true);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setIsRecording(false);
|
||||
if (keys.length < 4) onKeysChange(oldKeys);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
const key = e.key.toUpperCase();
|
||||
if (!keys.includes(key) && keys.length < 4) {
|
||||
onKeysChange([...keys, key]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex gap-2 min-h-[42px] items-center px-3 py-2 rounded-lg bg-background-80">
|
||||
<div className="flex flex-grow gap-2 flex-wrap">
|
||||
{keys.map((key, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="bg-accent-background-50 px-3 py-1 rounded-md text-sm"
|
||||
>
|
||||
{key}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="text-accent-background-10 text-right text-sm font-medium">
|
||||
{keys.length < 4 && isRecording ? l10n.getString('settings-sidebar_keybinds_now-recording') : l10n.getString('settings-sidebar_keybinds_record-keybind') }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
73
gui/src/components/commons/KeybindRow.tsx
Normal file
73
gui/src/components/commons/KeybindRow.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Controller, Control, UseFormResetField } from 'react-hook-form';
|
||||
import { Button } from './Button';
|
||||
import { NumberSelector } from './NumberSelector';
|
||||
import { KeybindRecorder } from './KeybindRecorder';
|
||||
import { useLocaleConfig } from '@/i18n/config';
|
||||
|
||||
export function KeybindRow({
|
||||
label,
|
||||
control,
|
||||
resetField,
|
||||
bindingName,
|
||||
delayName,
|
||||
}: {
|
||||
label: string;
|
||||
control: Control<any>;
|
||||
resetField: UseFormResetField<any>;
|
||||
bindingName: string;
|
||||
delayName: string;
|
||||
}) {
|
||||
const { currentLocales } = useLocaleConfig();
|
||||
const secondsFormat = new Intl.NumberFormat(currentLocales, {
|
||||
style: 'unit',
|
||||
unit: 'second',
|
||||
unitDisplay: 'narrow',
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
return (
|
||||
<tr className="border-b border-background-60 h-20">
|
||||
<td className="px-6 py-4 pr-4">
|
||||
<label className="text-sm font-medium text-background-10">
|
||||
{label}
|
||||
</label>
|
||||
</td>
|
||||
<td className="px-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name={bindingName}
|
||||
render={({ field }) => (
|
||||
<KeybindRecorder
|
||||
keys={field.value ?? []}
|
||||
onKeysChange={field.onChange}
|
||||
ref={field.ref}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4">
|
||||
<NumberSelector
|
||||
control={control}
|
||||
name={delayName}
|
||||
valueLabelFormat={(value) => secondsFormat.format(value)}
|
||||
min={0}
|
||||
max={10}
|
||||
step={0.2}
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td className="px-2">
|
||||
<div className="flex gap-2 justify-center px-4">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
resetField(bindingName);
|
||||
resetField(delayName);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
13
gui/src/components/commons/icon/ResetSettingIcon.tsx
Normal file
13
gui/src/components/commons/icon/ResetSettingIcon.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
export function ResetSettingIcon({ size = 24 }: { size?: number }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={size}
|
||||
viewBox="0 -960 960 960"
|
||||
width={size}
|
||||
fill="#00000"
|
||||
>
|
||||
<path d="M520-330v-60h160v60H520Zm60 210v-50h-60v-60h60v-50h60v160h-60Zm100-50v-60h160v60H680Zm40-110v-160h60v50h60v60h-60v50h-60Zm111-280h-83q-26-88-99-144t-169-56q-117 0-198.5 81.5T200-480q0 72 32.5 132t87.5 98v-110h80v240H160v-80h94q-62-50-98-122.5T120-480q0-75 28.5-140.5t77-114q48.5-48.5 114-77T480-840q129 0 226.5 79.5T831-560Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
313
gui/src/components/settings/pages/KeybindSettings.tsx
Normal file
313
gui/src/components/settings/pages/KeybindSettings.tsx
Normal file
@@ -0,0 +1,313 @@
|
||||
import { WrenchIcon } from '@/components/commons/icon/WrenchIcons';
|
||||
import {
|
||||
SettingsPageLayout,
|
||||
SettingsPagePaneLayout,
|
||||
} from '@/components/settings/SettingsPageLayout';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { ReactNode, useEffect } from 'react';
|
||||
import { KeybindRow } from '@/components/commons/KeybindRow';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import { useWebsocketAPI } from '@/hooks/websocket-api';
|
||||
import {
|
||||
KeybindRequestT,
|
||||
KeybindResponseT,
|
||||
RpcMessage,
|
||||
KeybindT,
|
||||
KeybindName,
|
||||
ChangeKeybindRequestT,
|
||||
} from 'solarxr-protocol';
|
||||
|
||||
export type KeybindsForm = {
|
||||
names: {
|
||||
fullResetName: KeybindName;
|
||||
yawResetName: KeybindName;
|
||||
mountingResetName: KeybindName;
|
||||
pauseTrackingName: KeybindName;
|
||||
feetResetName: KeybindName;
|
||||
};
|
||||
bindings: {
|
||||
fullResetBinding: string[];
|
||||
yawResetBinding: string[];
|
||||
mountingResetBinding: string[];
|
||||
pauseTrackingBinding: string[];
|
||||
feetResetBinding: string[];
|
||||
};
|
||||
delays: {
|
||||
fullResetDelay: number;
|
||||
yawResetDelay: number;
|
||||
mountingResetDelay: number;
|
||||
pauseTrackingDelay: number;
|
||||
feetResetDelay: number;
|
||||
};
|
||||
};
|
||||
|
||||
const defaultValues: KeybindsForm = {
|
||||
names: {
|
||||
fullResetName: KeybindName.FULL_RESET,
|
||||
yawResetName: KeybindName.YAW_RESET,
|
||||
mountingResetName: KeybindName.MOUNTING_RESET,
|
||||
pauseTrackingName: KeybindName.PAUSE_TRACKING,
|
||||
feetResetName: KeybindName.FEET_MOUNTING_RESET,
|
||||
},
|
||||
bindings: {
|
||||
fullResetBinding: ['CTRL', 'ALT', 'SHIFT', 'Y'],
|
||||
yawResetBinding: ['CTRL', 'ALT', 'SHIFT', 'U'],
|
||||
mountingResetBinding: ['CTRL', 'ALT', 'SHIFT', 'I'],
|
||||
pauseTrackingBinding: ['CTRL', 'ALT', 'SHIFT', 'O'],
|
||||
feetResetBinding: ['CTRL', 'ALT', 'SHIFT', 'P'],
|
||||
},
|
||||
delays: {
|
||||
fullResetDelay: 0,
|
||||
yawResetDelay: 0,
|
||||
mountingResetDelay: 0,
|
||||
pauseTrackingDelay: 0,
|
||||
feetResetDelay: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export function useKeybindsForm() {
|
||||
const {
|
||||
register,
|
||||
reset,
|
||||
handleSubmit,
|
||||
formState,
|
||||
control,
|
||||
getValues,
|
||||
resetField,
|
||||
watch,
|
||||
} = useForm<KeybindsForm>({
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
return {
|
||||
control,
|
||||
register,
|
||||
reset,
|
||||
handleSubmit,
|
||||
formState,
|
||||
getValues,
|
||||
resetField,
|
||||
watch,
|
||||
};
|
||||
}
|
||||
|
||||
export function KeybindSettings() {
|
||||
const { l10n } = useLocalization();
|
||||
const { control, reset, handleSubmit, watch, getValues, resetField } =
|
||||
useKeybindsForm();
|
||||
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
|
||||
|
||||
const onSubmit = (values: KeybindsForm) => {
|
||||
const keybinds = new ChangeKeybindRequestT();
|
||||
|
||||
const fullResetKeybind = new KeybindT();
|
||||
fullResetKeybind.keybindName = values.names.fullResetName;
|
||||
fullResetKeybind.keybindValue = values.bindings.fullResetBinding.join('+');
|
||||
fullResetKeybind.keybindDelay = values.delays.fullResetDelay;
|
||||
keybinds.keybind.push(fullResetKeybind);
|
||||
|
||||
const yawResetKeybind = new KeybindT();
|
||||
yawResetKeybind.keybindName = values.names.yawResetName;
|
||||
yawResetKeybind.keybindValue = values.bindings.yawResetBinding.join('+');
|
||||
yawResetKeybind.keybindDelay = values.delays.yawResetDelay;
|
||||
keybinds.keybind.push(yawResetKeybind);
|
||||
|
||||
const mountingResetKeybind = new KeybindT();
|
||||
mountingResetKeybind.keybindName = values.names.mountingResetName;
|
||||
mountingResetKeybind.keybindValue =
|
||||
values.bindings.mountingResetBinding.join('+');
|
||||
mountingResetKeybind.keybindDelay = values.delays.mountingResetDelay;
|
||||
keybinds.keybind.push(mountingResetKeybind);
|
||||
|
||||
const pauseTrackingKeybind = new KeybindT();
|
||||
pauseTrackingKeybind.keybindName = values.names.pauseTrackingName;
|
||||
pauseTrackingKeybind.keybindValue =
|
||||
values.bindings.pauseTrackingBinding.join('+');
|
||||
pauseTrackingKeybind.keybindDelay = values.delays.pauseTrackingDelay;
|
||||
keybinds.keybind.push(pauseTrackingKeybind);
|
||||
|
||||
const feetResetKeybind = new KeybindT();
|
||||
feetResetKeybind.keybindName = values.names.feetResetName;
|
||||
feetResetKeybind.keybindValue = values.bindings.feetResetBinding.join('+');
|
||||
feetResetKeybind.keybindDelay = values.delays.pauseTrackingDelay;
|
||||
keybinds.keybind.push(feetResetKeybind);
|
||||
|
||||
console.log(`Delay ${Number(fullResetKeybind.keybindDelay)}`);
|
||||
sendRPCPacket(RpcMessage.ChangeKeybindRequest, keybinds);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = watch(() => handleSubmit(onSubmit)());
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
sendRPCPacket(RpcMessage.KeybindRequest, new KeybindRequestT());
|
||||
}, []);
|
||||
|
||||
useRPCPacket(RpcMessage.KeybindResponse, ({ keybind }: KeybindResponseT) => {
|
||||
if (!keybind) return;
|
||||
|
||||
const keybindValues: KeybindsForm = {
|
||||
names: {
|
||||
fullResetName: KeybindName.FULL_RESET,
|
||||
yawResetName: KeybindName.YAW_RESET,
|
||||
mountingResetName: KeybindName.MOUNTING_RESET,
|
||||
pauseTrackingName: KeybindName.PAUSE_TRACKING,
|
||||
feetResetName: KeybindName.FEET_MOUNTING_RESET,
|
||||
},
|
||||
bindings: {
|
||||
fullResetBinding:
|
||||
(typeof keybind[KeybindName.FULL_RESET].keybindValue === 'string'
|
||||
? keybind[KeybindName.FULL_RESET].keybindValue
|
||||
: ''
|
||||
).split('+') || defaultValues.bindings.fullResetBinding,
|
||||
|
||||
yawResetBinding:
|
||||
(typeof keybind[KeybindName.YAW_RESET].keybindValue === 'string'
|
||||
? keybind[KeybindName.YAW_RESET].keybindValue
|
||||
: ''
|
||||
).split('+') || defaultValues.bindings.yawResetBinding,
|
||||
|
||||
mountingResetBinding:
|
||||
(typeof keybind[KeybindName.MOUNTING_RESET].keybindValue === 'string'
|
||||
? keybind[KeybindName.MOUNTING_RESET].keybindValue
|
||||
: ''
|
||||
).split('+') || defaultValues.bindings.mountingResetBinding,
|
||||
|
||||
pauseTrackingBinding:
|
||||
(typeof keybind[KeybindName.PAUSE_TRACKING].keybindValue === 'string'
|
||||
? keybind[KeybindName.PAUSE_TRACKING].keybindValue
|
||||
: ''
|
||||
).split('+') || defaultValues.bindings.pauseTrackingBinding,
|
||||
|
||||
feetResetBinding:
|
||||
(typeof keybind[KeybindName.FEET_MOUNTING_RESET].keybindValue ===
|
||||
'string'
|
||||
? keybind[KeybindName.FEET_MOUNTING_RESET].keybindValue
|
||||
: ''
|
||||
).split('+') || defaultValues.bindings.feetResetBinding,
|
||||
},
|
||||
delays: {
|
||||
fullResetDelay:
|
||||
keybind[KeybindName.FULL_RESET].keybindDelay ||
|
||||
defaultValues.delays.fullResetDelay,
|
||||
yawResetDelay:
|
||||
keybind[KeybindName.YAW_RESET].keybindDelay ||
|
||||
defaultValues.delays.yawResetDelay,
|
||||
mountingResetDelay:
|
||||
keybind[KeybindName.MOUNTING_RESET].keybindDelay ||
|
||||
defaultValues.delays.mountingResetDelay,
|
||||
pauseTrackingDelay:
|
||||
keybind[KeybindName.PAUSE_TRACKING].keybindDelay ||
|
||||
defaultValues.delays.mountingResetDelay,
|
||||
feetResetDelay:
|
||||
keybind[KeybindName.FEET_MOUNTING_RESET].keybindDelay ||
|
||||
defaultValues.delays.feetResetDelay,
|
||||
},
|
||||
};
|
||||
|
||||
reset({ ...getValues(), ...keybindValues });
|
||||
});
|
||||
|
||||
const handleResetButton = () => {
|
||||
reset(defaultValues);
|
||||
};
|
||||
|
||||
function Table({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<table className="min-w-full divide-y divide-background-50">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" className="px-6 py-3 text-start">
|
||||
<Localized id={'keybind_config-keybind_name'}>
|
||||
<Typography />
|
||||
</Localized>
|
||||
Keybind
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-middle">
|
||||
<Localized id={'keybind_config-keybind_value'}>
|
||||
<Typography />
|
||||
</Localized>
|
||||
Combination
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-middle">
|
||||
<Localized id={'keybind_config-keybind_delay'}>
|
||||
<Typography />
|
||||
</Localized>
|
||||
Delay before trigger (S)
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{children}</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsPageLayout>
|
||||
<form className="flex flex-col gap-2 w-full">
|
||||
<SettingsPagePaneLayout icon={<WrenchIcon />} id="keybinds">
|
||||
<>
|
||||
<Typography variant="main-title" id="settings-keybinds" />
|
||||
<div className="flex flex-col pt-2 pb-4">
|
||||
{l10n
|
||||
.getString('settings-keybinds-description')
|
||||
.split('\n')
|
||||
.map((line, i) => (
|
||||
<Typography key={i}>{line}</Typography>
|
||||
))}
|
||||
</div>
|
||||
<Table>
|
||||
<KeybindRow
|
||||
label="Full Reset"
|
||||
control={control}
|
||||
resetField={resetField}
|
||||
bindingName="bindings.fullResetBinding"
|
||||
delayName="delays.fullResetDelay"
|
||||
/>
|
||||
<KeybindRow
|
||||
label="Yaw Reset"
|
||||
control={control}
|
||||
resetField={resetField}
|
||||
bindingName="bindings.yawResetBinding"
|
||||
delayName="delays.yawResetDelay"
|
||||
/>
|
||||
<KeybindRow
|
||||
label="Mounting Reset"
|
||||
control={control}
|
||||
resetField={resetField}
|
||||
bindingName="bindings.mountingResetBinding"
|
||||
delayName="delays.mountingResetDelay"
|
||||
/>
|
||||
<KeybindRow
|
||||
label="Feet Mounting Reset"
|
||||
control={control}
|
||||
resetField={resetField}
|
||||
bindingName="bindings.feetResetBinding"
|
||||
delayName="delays.feetResetDelay"
|
||||
/>
|
||||
<KeybindRow
|
||||
label="Pause Tracking"
|
||||
control={control}
|
||||
resetField={resetField}
|
||||
bindingName="bindings.pauseTrackingBinding"
|
||||
delayName="delays.pauseTrackingDelay"
|
||||
/>
|
||||
</Table>
|
||||
<div className="flex flex-col pt-4" />
|
||||
<Button
|
||||
className="flex flex-col"
|
||||
onClick={handleResetButton}
|
||||
variant="primary"
|
||||
>
|
||||
Reset All
|
||||
</Button>
|
||||
</>
|
||||
</SettingsPagePaneLayout>
|
||||
</form>
|
||||
</SettingsPageLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package dev.slimevr.keybind
|
||||
|
||||
import dev.slimevr.VRServer
|
||||
import dev.slimevr.config.KeybindingsConfig
|
||||
import solarxr_protocol.rpc.Keybind
|
||||
import solarxr_protocol.rpc.KeybindName
|
||||
import solarxr_protocol.rpc.KeybindT
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
|
||||
class KeybindHandler(val vrServer: VRServer) {
|
||||
private val listeners: MutableList<KeybindListener> = CopyOnWriteArrayList()
|
||||
var keybinds: MutableList<KeybindT> = mutableListOf()
|
||||
|
||||
init {
|
||||
createKeybinds()
|
||||
}
|
||||
|
||||
fun sendKeybinds(KeybindName: String) {
|
||||
this.listeners.forEach { it.sendKeybind()}
|
||||
}
|
||||
|
||||
fun addListener(listener: KeybindListener) {
|
||||
this.listeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeListener(listener: KeybindListener) {
|
||||
listeners.removeIf { listener == it }
|
||||
}
|
||||
|
||||
private fun createKeybinds() {
|
||||
keybinds.clear()
|
||||
keybinds.add(
|
||||
KeybindT().apply {
|
||||
keybindName = KeybindName.FULL_RESET
|
||||
keybindValue = vrServer.configManager.vrConfig.keybindings.fullResetBinding
|
||||
keybindDelay = vrServer.configManager.vrConfig.keybindings.fullResetDelay
|
||||
},
|
||||
)
|
||||
keybinds.add(
|
||||
KeybindT().apply {
|
||||
keybindName = KeybindName.YAW_RESET
|
||||
keybindValue = vrServer.configManager.vrConfig.keybindings.yawResetBinding
|
||||
keybindDelay = vrServer.configManager.vrConfig.keybindings.yawResetDelay
|
||||
},
|
||||
)
|
||||
keybinds.add(
|
||||
KeybindT().apply {
|
||||
keybindName = KeybindName.MOUNTING_RESET
|
||||
keybindValue = vrServer.configManager.vrConfig.keybindings.mountingResetBinding
|
||||
keybindDelay = vrServer.configManager.vrConfig.keybindings.mountingResetDelay
|
||||
},
|
||||
)
|
||||
keybinds.add(
|
||||
KeybindT().apply {
|
||||
keybindName = KeybindName.PAUSE_TRACKING
|
||||
keybindValue = vrServer.configManager.vrConfig.keybindings.pauseTrackingBinding
|
||||
keybindDelay = vrServer.configManager.vrConfig.keybindings.pauseTrackingDelay
|
||||
},
|
||||
)
|
||||
keybinds.add(
|
||||
KeybindT().apply {
|
||||
keybindName = KeybindName.PAUSE_TRACKING
|
||||
keybindValue = vrServer.configManager.vrConfig.keybindings.feetMountingResetBinding
|
||||
keybindDelay = vrServer.configManager.vrConfig.keybindings.feetMountingResetDelay
|
||||
}
|
||||
)
|
||||
}
|
||||
//TODO: Maybe recreating all the keybinds isn't the best idea?
|
||||
fun updateKeybinds() {
|
||||
createKeybinds()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package dev.slimevr.keybind
|
||||
|
||||
interface KeybindListener {
|
||||
fun sendKeybind()
|
||||
fun onKeybindUpdate()
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package dev.slimevr.keybinding
|
||||
|
||||
import org.freedesktop.dbus.DBusPath
|
||||
import org.freedesktop.dbus.annotations.DBusInterfaceName
|
||||
import org.freedesktop.dbus.interfaces.DBusInterface
|
||||
import org.freedesktop.dbus.messages.DBusSignal
|
||||
import org.freedesktop.dbus.types.Variant
|
||||
|
||||
@DBusInterfaceName("org.freedesktop.portal.GlobalKeybinds")
|
||||
interface GlobalKeybinds : DBusInterface {
|
||||
|
||||
// Creates a session for the shortcuts
|
||||
fun CreateSession(options: Map<String, Variant<*>>): DBusPath
|
||||
|
||||
fun BindShortcuts(
|
||||
sessionHandle: DBusPath,
|
||||
shortcuts: Array<Shortcut>,
|
||||
parentWindow: String,
|
||||
options: Map<String, Variant<*>>
|
||||
): DBusPath
|
||||
|
||||
class Activated(
|
||||
path: String,
|
||||
val sessionHandle: DBusPath,
|
||||
val shortcutId: String,
|
||||
val timestamp: Long,
|
||||
val options: Map<String, Variant<*>>
|
||||
) : DBusSignal(path, sessionHandle, shortcutId, timestamp, options)
|
||||
}
|
||||
|
||||
data class Shortcut(val id: String, val properties: Map<String, Variant<*>>)
|
||||
148
server/core/src/main/java/dev/slimevr/keybinding/Keybinding.kt
Normal file
148
server/core/src/main/java/dev/slimevr/keybinding/Keybinding.kt
Normal file
@@ -0,0 +1,148 @@
|
||||
package dev.slimevr.keybinding
|
||||
|
||||
import com.melloware.jintellitype.HotkeyListener
|
||||
import com.melloware.jintellitype.JIntellitype
|
||||
import dev.slimevr.VRServer
|
||||
import dev.slimevr.config.KeybindingsConfig
|
||||
import dev.slimevr.tracking.trackers.TrackerUtils
|
||||
import io.eiren.util.OperatingSystem
|
||||
import io.eiren.util.ann.AWTThread
|
||||
import io.eiren.util.logging.LogManager
|
||||
|
||||
class Keybinding @AWTThread constructor(val server: VRServer) : HotkeyListener {
|
||||
val config: KeybindingsConfig = server.configManager.vrConfig.keybindings
|
||||
|
||||
init {
|
||||
|
||||
if (OperatingSystem.Companion.currentPlatform != OperatingSystem.WINDOWS) {
|
||||
LogManager
|
||||
.info(
|
||||
"[Keybinding] Currently only supported on Windows. Keybindings will be disabled.",
|
||||
)
|
||||
|
||||
/*
|
||||
try {
|
||||
val connection = DBusConnectionBuilder.forSessionBus().build()
|
||||
|
||||
// 1. Get the Portal object
|
||||
val portal = connection.getRemoteObject(
|
||||
"org.freedesktop.portal.Desktop",
|
||||
"/org/freedesktop/portal/desktop",
|
||||
GlobalKeybinds::class.java
|
||||
)
|
||||
|
||||
// 2. Setup the Signal Listener
|
||||
connection.addSigHandler(GlobalKeybinds.Activated::class.java) { signal ->
|
||||
when (signal.shortcutId) {
|
||||
"my_cool_action" -> println("🚀 Hotkey Triggered!")
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Create Session & Bind (Simplified)
|
||||
// Note: In a real app, you'd handle the ObjectPath callbacks
|
||||
// for CreateSession before calling BindShortcuts.
|
||||
val shortcuts = arrayOf(
|
||||
Shortcut(
|
||||
"my_cool_action",
|
||||
mapOf("description" to Variant("Open My App"))
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
// This triggers the OS permissions popup
|
||||
portal.BindShortcuts(, shortcuts, "", emptyMap())
|
||||
} catch (e: Error) {
|
||||
println("Dbus error: ${e}")
|
||||
}
|
||||
|
||||
*/
|
||||
} else {
|
||||
try {
|
||||
if (JIntellitype.getInstance() != null) {
|
||||
JIntellitype.getInstance().addHotKeyListener(this)
|
||||
|
||||
val fullResetBinding = config.fullResetBinding
|
||||
JIntellitype.getInstance()
|
||||
.registerHotKey(FULL_RESET, fullResetBinding)
|
||||
LogManager.info("[Keybinding] Bound full reset to $fullResetBinding")
|
||||
|
||||
val yawResetBinding = config.yawResetBinding
|
||||
JIntellitype.getInstance()
|
||||
.registerHotKey(YAW_RESET, yawResetBinding)
|
||||
LogManager.info("[Keybinding] Bound yaw reset to $yawResetBinding")
|
||||
|
||||
val mountingResetBinding = config.mountingResetBinding
|
||||
JIntellitype.getInstance()
|
||||
.registerHotKey(MOUNTING_RESET, mountingResetBinding)
|
||||
LogManager.info("[Keybinding] Bound reset mounting to $mountingResetBinding")
|
||||
|
||||
val feetMountingResetBinding = config.feetMountingResetBinding
|
||||
JIntellitype.getInstance()
|
||||
.registerHotKey(FEET_MOUNTING_RESET, feetMountingResetBinding)
|
||||
LogManager.info("[Keybinding] Bound feet reset mounting to $feetMountingResetBinding")
|
||||
|
||||
val pauseTrackingBinding = config.pauseTrackingBinding
|
||||
JIntellitype.getInstance()
|
||||
.registerHotKey(PAUSE_TRACKING, pauseTrackingBinding)
|
||||
LogManager.info("[Keybinding] Bound pause tracking to $pauseTrackingBinding")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
LogManager
|
||||
.warning(
|
||||
"[Keybinding] JIntellitype initialization failed. Keybindings will be disabled. Try restarting your computer.",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AWTThread
|
||||
override fun onHotKey(identifier: Int) {
|
||||
when (identifier) {
|
||||
FULL_RESET -> server.scheduleResetTrackersFull(
|
||||
RESET_SOURCE_NAME,
|
||||
(config.fullResetDelay * 1000).toLong()
|
||||
)
|
||||
|
||||
YAW_RESET -> server.scheduleResetTrackersYaw(
|
||||
RESET_SOURCE_NAME,
|
||||
(config.yawResetDelay * 1000).toLong()
|
||||
)
|
||||
|
||||
MOUNTING_RESET -> server.scheduleResetTrackersMounting(
|
||||
RESET_SOURCE_NAME,
|
||||
(config.mountingResetDelay * 1000).toLong()
|
||||
)
|
||||
|
||||
FEET_MOUNTING_RESET -> server.scheduleResetTrackersMounting(
|
||||
RESET_SOURCE_NAME,
|
||||
(config.feetMountingResetDelay * 1000).toLong(),
|
||||
TrackerUtils.feetsBodyParts,
|
||||
)
|
||||
|
||||
PAUSE_TRACKING ->
|
||||
server
|
||||
.scheduleTogglePauseTracking(
|
||||
RESET_SOURCE_NAME,
|
||||
(config.pauseTrackingDelay * 1000).toLong(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum class KeybindName {
|
||||
FULL_RESET,
|
||||
YAW_RESET,
|
||||
MOUNTING_RESET,
|
||||
PAUSE_TRACKING
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val RESET_SOURCE_NAME = "Keybinding"
|
||||
|
||||
private const val FULL_RESET = 1
|
||||
private const val YAW_RESET = 2
|
||||
private const val MOUNTING_RESET = 3
|
||||
private const val FEET_MOUNTING_RESET = 4
|
||||
private const val PAUSE_TRACKING = 5
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package dev.slimevr.protocol.rpc.keybinds
|
||||
|
||||
import com.google.flatbuffers.FlatBufferBuilder
|
||||
import dev.slimevr.keybinding.Keybinding
|
||||
import dev.slimevr.keybind.KeybindListener
|
||||
import dev.slimevr.protocol.GenericConnection
|
||||
import dev.slimevr.protocol.rpc.RPCHandler
|
||||
import dev.slimevr.protocol.ProtocolAPI
|
||||
import jdk.internal.joptsimple.internal.Messages.message
|
||||
import solarxr_protocol.rpc.ChangeKeybindRequest
|
||||
import solarxr_protocol.rpc.KeybindName
|
||||
import solarxr_protocol.rpc.KeybindRequest
|
||||
import solarxr_protocol.rpc.KeybindResponse
|
||||
import solarxr_protocol.rpc.KeybindResponseT
|
||||
import solarxr_protocol.rpc.KeybindT
|
||||
import solarxr_protocol.rpc.RpcMessage
|
||||
import solarxr_protocol.rpc.RpcMessageHeader
|
||||
|
||||
class RPCKeybindHandler(
|
||||
var rpcHandler: RPCHandler,
|
||||
var api: ProtocolAPI
|
||||
) : KeybindListener {
|
||||
|
||||
val keybindingConfig = api.server.configManager.vrConfig.keybindings
|
||||
|
||||
init {
|
||||
this.api.server.keybindHandler.addListener(this)
|
||||
|
||||
rpcHandler.registerPacketListener(RpcMessage.KeybindRequest, ::onKeybindRequest)
|
||||
rpcHandler.registerPacketListener(RpcMessage.ChangeKeybindRequest, ::onChangeKeybindRequest)
|
||||
}
|
||||
|
||||
//TODO: Figure out a way to "refresh" the keybind array here.
|
||||
private fun buildKeybindResponse(fbb: FlatBufferBuilder) : Int = KeybindResponse.pack(
|
||||
fbb,
|
||||
KeybindResponseT().apply {
|
||||
keybind = api.server.keybindHandler.keybinds.toTypedArray()
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
private fun onKeybindRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) {
|
||||
val fbb = FlatBufferBuilder(32)
|
||||
val response = buildKeybindResponse(fbb)
|
||||
val outbound = rpcHandler.createRPCMessage(
|
||||
fbb,
|
||||
RpcMessage.KeybindResponse,
|
||||
response,
|
||||
)
|
||||
fbb.finish(outbound)
|
||||
conn.send(fbb.dataBuffer())
|
||||
}
|
||||
|
||||
private fun onChangeKeybindRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) {
|
||||
val req = (messageHeader.message(ChangeKeybindRequest()) as ChangeKeybindRequest).unpack()
|
||||
|
||||
keybindingConfig.fullResetBinding = req.keybind[KeybindName.FULL_RESET].keybindValue
|
||||
keybindingConfig.fullResetDelay = req.keybind[KeybindName.FULL_RESET].keybindDelay
|
||||
|
||||
keybindingConfig.yawResetBinding = req.keybind[KeybindName.YAW_RESET].keybindValue
|
||||
keybindingConfig.yawResetDelay = req.keybind[KeybindName.YAW_RESET].keybindDelay
|
||||
|
||||
keybindingConfig.mountingResetBinding = req.keybind[KeybindName.MOUNTING_RESET].keybindValue
|
||||
keybindingConfig.mountingResetDelay = req.keybind[KeybindName.MOUNTING_RESET].keybindDelay
|
||||
|
||||
keybindingConfig.pauseTrackingBinding = req.keybind[KeybindName.PAUSE_TRACKING].keybindValue
|
||||
keybindingConfig.pauseTrackingDelay = req.keybind[KeybindName.PAUSE_TRACKING].keybindDelay
|
||||
|
||||
keybindingConfig.feetMountingResetBinding = req.keybind[KeybindName.FEET_MOUNTING_RESET].keybindValue
|
||||
keybindingConfig.feetMountingResetDelay = req.keybind[KeybindName.FEET_MOUNTING_RESET].keybindDelay
|
||||
|
||||
api.server.configManager.saveConfig()
|
||||
api.server.keybindHandler.updateKeybinds()
|
||||
}
|
||||
override fun onKeybindUpdate() {
|
||||
/*
|
||||
val fbb = FlatBufferBuilder(32)
|
||||
val response = buildKeybindResponse(fbb)
|
||||
val outbound = rpcHandler.createRPCMessage(
|
||||
fbb,
|
||||
RpcMessage.KeybindResponse,
|
||||
response
|
||||
)
|
||||
fbb.finish(outbound)
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
|
||||
override fun sendKeybind() {
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val FULL_RESET = KeybindName.FULL_RESET
|
||||
const val YAW_RESET = KeybindName.YAW_RESET
|
||||
const val MOUNTING_RESET = KeybindName.MOUNTING_RESET
|
||||
const val PAUSE_TRACKING = KeybindName.PAUSE_TRACKING
|
||||
const val FEET_MOUNTING_RESET = KeybindName.FEET_MOUNTING_RESET
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user