-
- {doubleStep !== undefined && (
+ render={({ field: { onChange, value } }) => {
+ return (
+
+ {label?.length != 0 ?
{label} : <>>}
+
+
+ {doubleStep !== undefined && (
+
+ )}
- )}
-
-
-
- {valueLabelFormat ? valueLabelFormat(value) : value}
-
-
-
- {doubleStep !== undefined && (
+
+
+ {valueLabelFormat ? valueLabelFormat(value) : value}
+
+
- )}
+ {doubleStep !== undefined && (
+
+ )}
+
-
- )}
+ );
+ }}
/>
);
}
diff --git a/gui/src/components/commons/icon/ResetSettingIcon.tsx b/gui/src/components/commons/icon/ResetSettingIcon.tsx
new file mode 100644
index 0000000000..a1588e3eac
--- /dev/null
+++ b/gui/src/components/commons/icon/ResetSettingIcon.tsx
@@ -0,0 +1,13 @@
+export function ResetSettingIcon({ size = 24 }: { size?: number }) {
+ return (
+
+ );
+}
diff --git a/gui/src/components/onboarding/UdevRulesModal.tsx b/gui/src/components/onboarding/UdevRulesModal.tsx
index b89af1e733..bce273972f 100644
--- a/gui/src/components/onboarding/UdevRulesModal.tsx
+++ b/gui/src/components/onboarding/UdevRulesModal.tsx
@@ -4,21 +4,19 @@ import { BaseModal } from '@/components/commons/BaseModal';
import { CheckboxInternal } from '@/components/commons/Checkbox';
import { Typography } from '@/components/commons/Typography';
import { useElectron } from '@/hooks/electron';
-import { useWebsocketAPI } from '@/hooks/websocket-api';
-import { RpcMessage, InstalledInfoResponseT } from 'solarxr-protocol';
import { useConfig } from '@/hooks/config';
import { useLocalization } from '@fluent/react';
+import { useAppContext } from '@/hooks/app';
export function UdevRulesModal() {
const { config, setConfig } = useConfig();
- const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
const electron = useElectron();
const [udevContent, setUdevContent] = useState('');
- const [isUdevInstalledResponse, setIsUdevInstalledResponse] = useState(true);
const [showUdevWarning, setShowUdevWarning] = useState(false);
const [dontShowThisSession, setDontShowThisSession] = useState(false);
const [dontShowAgain, setDontShowAgain] = useState(false);
const { l10n } = useLocalization();
+ const { installInfo } = useAppContext();
const handleUdevContent = async () => {
if (electron.isElectron) {
@@ -38,28 +36,14 @@ export function UdevRulesModal() {
if (!config) throw 'Invalid state!';
if (electron.isElectron) {
const isLinux = electron.data().os.type === 'linux';
- const udevMissing = !isUdevInstalledResponse;
+ const udevMissing = !installInfo?.isUdevInstalled;
const notHiddenGlobally = !config.dontShowUdevModal;
const notHiddenThisSession = !dontShowThisSession;
const shouldShow =
isLinux && udevMissing && notHiddenGlobally && notHiddenThisSession;
setShowUdevWarning(shouldShow);
}
- }, [config, isUdevInstalledResponse, dontShowThisSession]);
-
- useEffect(() => {
- sendRPCPacket(
- RpcMessage.InstalledInfoRequest,
- new InstalledInfoResponseT()
- );
- }, []);
-
- useRPCPacket(
- RpcMessage.InstalledInfoResponse,
- ({ isUdevInstalled }: InstalledInfoResponseT) => {
- setIsUdevInstalledResponse(isUdevInstalled);
- }
- );
+ }, [config, dontShowThisSession]);
const handleModalClose = () => {
if (!config) throw 'Invalid State!';
@@ -72,7 +56,7 @@ export function UdevRulesModal() {
};
return (
-
+
diff --git a/gui/src/components/settings/SettingsLayout.tsx b/gui/src/components/settings/SettingsLayout.tsx
index 7bfc062ef8..ca148f86ca 100644
--- a/gui/src/components/settings/SettingsLayout.tsx
+++ b/gui/src/components/settings/SettingsLayout.tsx
@@ -58,6 +58,10 @@ export function SettingSelectorMobile() {
label: l10n.getString('settings-sidebar-advanced'),
value: { url: '/settings/advanced' },
},
+ {
+ label: l10n.getString('settings-sidebar-keybinds'),
+ value: { url: '/settings/keybinds' },
+ },
{
label: l10n.getString('navbar-onboarding'),
value: { url: '/onboarding/home' },
diff --git a/gui/src/components/settings/SettingsSidebar.tsx b/gui/src/components/settings/SettingsSidebar.tsx
index 1095073394..4a9837759f 100644
--- a/gui/src/components/settings/SettingsSidebar.tsx
+++ b/gui/src/components/settings/SettingsSidebar.tsx
@@ -73,6 +73,13 @@ export function SettingsSidebar() {
scrollTo="gestureControl"
id="settings-sidebar-gesture_control"
/>
+ {
+
+ }
diff --git a/gui/src/components/settings/pages/KeybindSettings.scss b/gui/src/components/settings/pages/KeybindSettings.scss
new file mode 100644
index 0000000000..b73610c1a0
--- /dev/null
+++ b/gui/src/components/settings/pages/KeybindSettings.scss
@@ -0,0 +1,15 @@
+.keybind-settings {
+ display: grid;
+
+ grid-template:
+ 'n v d ' auto
+ 'k k k ' auto
+ 'b . . ' auto
+ / 1fr 1fr 90px;
+
+ grid-template-columns: subgrid;
+ grid-template-rows: subgrid;
+
+ align-items: left;
+ gap: 10px;
+}
diff --git a/gui/src/components/settings/pages/KeybindSettings.tsx b/gui/src/components/settings/pages/KeybindSettings.tsx
new file mode 100644
index 0000000000..f9b2ba5fb7
--- /dev/null
+++ b/gui/src/components/settings/pages/KeybindSettings.tsx
@@ -0,0 +1,269 @@
+import { KeybindRecorderModal } from '@/components/commons/KeybindRecorderModal';
+import {
+ SettingsPageLayout,
+ SettingsPagePaneLayout,
+} from '@/components/settings/SettingsPageLayout';
+import { WrenchIcon } from '@/components/commons/icon/WrenchIcons';
+import { Typography } from '@/components/commons/Typography';
+import { useLocalization } from '@fluent/react';
+import './KeybindSettings.scss';
+import { Button } from '@/components/commons/Button';
+import { KeybindsRow } from '@/components/commons/KeybindsRow';
+import { useWebsocketAPI } from '@/hooks/websocket-api';
+import { ReactNode, useEffect, useRef, useState } from 'react';
+import {
+ ChangeKeybindRequestT,
+ KeybindRequestT,
+ KeybindResponseT,
+ KeybindT,
+ OpenUriRequestT,
+ RpcMessage,
+} from 'solarxr-protocol';
+import {
+ FieldPath,
+ FormProvider,
+ useFieldArray,
+ useForm,
+} from 'react-hook-form';
+import { useAppContext } from '@/hooks/app';
+import { useElectron } from '@/hooks/electron';
+
+export type KeybindForm = {
+ keybinds: {
+ id: number;
+ name: string;
+ binding: string[];
+ delay: number;
+ }[];
+};
+
+export function KeybindSettings() {
+ const electron = useElectron();
+ const { l10n } = useLocalization();
+ const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
+ const [isOpen, setIsOpen] = useState
(false);
+ const [defaultKeybindsState, setDefaultKeybindsState] = useState(
+ {
+ keybinds: [],
+ }
+ );
+ const currentIndex = useRef(null);
+ const { installInfo } = useAppContext();
+
+ const methods = useForm({
+ defaultValues: defaultKeybindsState,
+ });
+
+ const {
+ control,
+ handleSubmit,
+ reset,
+ setValue,
+ getValues,
+ setError,
+ clearErrors,
+ resetField,
+ } = methods;
+
+ const { fields } = useFieldArray({
+ control,
+ name: 'keybinds',
+ });
+
+ const onSubmit = () => {
+ const value = getValues();
+ if (checkDuplicates(value)) {
+ return;
+ }
+ clearErrors('keybinds');
+
+ value.keybinds.forEach((kb) => {
+ const changeKeybindRequest = new ChangeKeybindRequestT();
+
+ const keybind = new KeybindT();
+ keybind.keybindId = kb.id;
+ keybind.keybindNameId = kb.name;
+ keybind.keybindValue = kb.binding.join('+');
+ keybind.keybindDelay = kb.delay;
+
+ changeKeybindRequest.keybind = keybind;
+
+ sendRPCPacket(RpcMessage.ChangeKeybindRequest, changeKeybindRequest);
+ setIsOpen(false);
+ });
+ };
+
+ const checkDuplicates = (value: KeybindForm) => {
+ const normalized = value.keybinds
+ .filter((kb) => kb.binding.length > 0)
+ .map((kb) => JSON.stringify([...kb.binding].sort()));
+
+ const unique = new Set(normalized);
+
+ if (unique.size !== normalized.length) {
+ setError('keybinds', {
+ type: 'manual',
+ message: 'Duplicate keybind combinations are not allowed',
+ });
+ return true;
+ }
+
+ return false;
+ };
+
+ const handleOpenSystemSettingsButton = () => {
+ sendRPCPacket(RpcMessage.OpenUriRequest, new OpenUriRequestT());
+ };
+
+ useRPCPacket(
+ RpcMessage.KeybindResponse,
+ ({ keybind, defaultKeybinds }: KeybindResponseT) => {
+ if (!keybind) return;
+
+ const mappedDefaults = defaultKeybinds.map((kb) => ({
+ id: kb.keybindId,
+ name: typeof kb.keybindNameId === 'string' ? kb.keybindNameId : '',
+ binding:
+ typeof kb.keybindValue === 'string' ? kb.keybindValue.split('+') : [],
+ delay: kb.keybindDelay,
+ }));
+
+ setDefaultKeybindsState({ keybinds: mappedDefaults });
+ reset({ keybinds: mappedDefaults });
+
+ const mapped = keybind.map((kb) => ({
+ id: kb.keybindId,
+ name: typeof kb.keybindNameId === 'string' ? kb.keybindNameId : '',
+ binding:
+ typeof kb.keybindValue === 'string' ? kb.keybindValue.split('+') : [],
+ delay: kb.keybindDelay,
+ }));
+
+ mapped.forEach((keybind, index) => {
+ setValue(`keybinds.${index}.binding`, keybind.binding);
+ setValue(`keybinds.${index}.delay`, keybind.delay);
+ });
+ }
+ );
+
+ const handleOpenRecorderModal = (index: number) => {
+ currentIndex.current = index;
+ if (currentIndex !== null) {
+ setIsOpen(true);
+ }
+ };
+
+ const onClose = () => {
+ if (currentIndex.current != null) {
+ resetField(`keybinds.${currentIndex.current}.binding`);
+ }
+ setIsOpen(false);
+ };
+
+ const createKeybindRows = (): ReactNode => {
+ return fields.map((field, index) => {
+ return (
+
+
+
+ );
+ });
+ };
+
+ useEffect(() => {
+ sendRPCPacket(RpcMessage.KeybindRequest, new KeybindRequestT());
+ }, []);
+
+ return (
+
+ } id="keybinds">
+
+
+
+ {l10n
+ .getString('settings-keybinds-description')
+ .split('\n')
+ .map((line, i) => (
+ {line}
+ ))}
+
+ {!installInfo?.isWayland ? (
+
+ ) : (
+ electron.isElectron &&
+ electron.data().os.type !== 'windows' && (
+ <>
+
+
+
+
+
+ {createKeybindRows()}
+
+
+
+
+ }
+ isVisisble={isOpen}
+ onClose={onClose}
+ onUnbind={() => {
+ if (currentIndex.current != null)
+ setValue(
+ `keybinds.${currentIndex.current}.binding`,
+ []
+ );
+ }}
+ onSubmit={onSubmit}
+ />
+
+ >
+ )
+ )}
+
+
+
+ );
+}
diff --git a/gui/src/hooks/app.ts b/gui/src/hooks/app.ts
index 0623db19bb..7b0b58b3e0 100644
--- a/gui/src/hooks/app.ts
+++ b/gui/src/hooks/app.ts
@@ -5,6 +5,7 @@ import {
ResetResponseT,
RpcMessage,
StartDataFeedT,
+ InstalledInfoResponseT,
} from 'solarxr-protocol';
import { handleResetSounds } from '@/sounds/sounds';
import { useConfig } from './config';
@@ -18,11 +19,17 @@ import { DEFAULT_LOCALE, LangContext } from '@/i18n/config';
export interface AppContext {
currentFirmwareRelease: FirmwareRelease | null;
+ installInfo: InstalledInfoResponseT | null;
}
export function useProvideAppContext(): AppContext {
- const { useRPCPacket, sendDataFeedPacket, useDataFeedPacket, isConnected } =
- useWebsocketAPI();
+ const {
+ useRPCPacket,
+ sendRPCPacket,
+ sendDataFeedPacket,
+ useDataFeedPacket,
+ isConnected,
+ } = useWebsocketAPI();
const { changeLocales } = useContext(LangContext);
const { config } = useConfig();
const { dataFeedConfig } = useDataFeedConfig();
@@ -34,6 +41,8 @@ export function useProvideAppContext(): AppContext {
const [currentFirmwareRelease, setCurrentFirmwareRelease] =
useState(null);
+ const [installInfo, setInstallInfo] = useState(null);
+
useEffect(() => {
if (isConnected) {
const startDataFeed = new StartDataFeedT();
@@ -70,6 +79,17 @@ export function useProvideAppContext(): AppContext {
};
}, [config?.uuid]);
+ useEffect(() => {
+ sendRPCPacket(RpcMessage.InstalledInfoRequest, new InstalledInfoResponseT());
+ }, []);
+
+ useRPCPacket(
+ RpcMessage.InstalledInfoResponse,
+ ({ isUdevInstalled, isWayland }: InstalledInfoResponseT) => {
+ setInstallInfo(new InstalledInfoResponseT(isUdevInstalled, isWayland));
+ }
+ );
+
useLayoutEffect(() => {
changeLocales([config?.lang || DEFAULT_LOCALE]);
}, []);
@@ -85,6 +105,7 @@ export function useProvideAppContext(): AppContext {
return {
currentFirmwareRelease,
+ installInfo,
};
}
diff --git a/gui/tailwind.config.ts b/gui/tailwind.config.ts
index 07004e88cd..26c3b00a58 100644
--- a/gui/tailwind.config.ts
+++ b/gui/tailwind.config.ts
@@ -183,6 +183,7 @@ const config = {
lg: '1300px',
xl: '1600px',
tall: { raw: '(min-height: 860px)' },
+ 'keybinds-small': { raw: 'not (min-width: 1230px)' },
},
extend: {
colors: {
diff --git a/server/core/build.gradle.kts b/server/core/build.gradle.kts
index 6e213498b3..2f84826f7d 100644
--- a/server/core/build.gradle.kts
+++ b/server/core/build.gradle.kts
@@ -83,6 +83,8 @@ dependencies {
implementation("com.mayakapps.kache:kache:2.1.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
+ implementation("com.github.HannahPadd:DbusGlobalShortcutsWayland:v0.1.0")
+
api("com.github.loucass003:EspflashKotlin:v0.11.0")
// Allow the use of reflection
diff --git a/server/core/src/main/java/dev/slimevr/Keybinding.kt b/server/core/src/main/java/dev/slimevr/Keybinding.kt
index 9f20f768a8..f0d246a963 100644
--- a/server/core/src/main/java/dev/slimevr/Keybinding.kt
+++ b/server/core/src/main/java/dev/slimevr/Keybinding.kt
@@ -2,51 +2,49 @@ package dev.slimevr
import com.melloware.jintellitype.HotkeyListener
import com.melloware.jintellitype.JIntellitype
+import dev.hannah.portals.PortalManager
+import dev.hannah.portals.globalShortcuts.Shortcut
+import dev.hannah.portals.globalShortcuts.ShortcutTuple
import dev.slimevr.config.KeybindingsConfig
import dev.slimevr.tracking.trackers.TrackerUtils
import io.eiren.util.OperatingSystem
import io.eiren.util.OperatingSystem.Companion.currentPlatform
import io.eiren.util.ann.AWTThread
import io.eiren.util.logging.LogManager
+import solarxr_protocol.rpc.KeybindId
+
+enum class Keybinds(val id: Int, val keybindName: String, val description: String, val keybind: String) {
+ FULL_RESET(KeybindId.FULL_RESET, "FULL_RESET", "Full Reset", "CTRL+ALT+SHIFT+Y"),
+ MOUNTING_RESET(KeybindId.MOUNTING_RESET, "MOUNTING_RESET", "Mounting Reset", "CTRL+ALT+SHIFT+I"),
+ PAUSE_TRACKING(KeybindId.PAUSE_TRACKING, "PAUSE_TRACKING", "Pause Tracking", "CTRL+ALT+SHIFT+O"),
+ YAW_RESET(KeybindId.YAW_RESET, "YAW_RESET", "Yaw Reset", "CTRL+ALT+SHIFT+U"),
+ FEET_MOUNTING_RESET(KeybindId.FEET_MOUNTING_RESET, "FEET_MOUNTING_RESET", "Feet Mounting Reset", "CTRL+ALT+SHIFT+P"),
+ ;
+
+ override fun toString(): String = keybindName
+
+ companion object {
+ private val byId = Keybinds.entries.associateBy { it.id }
+ private val byName = Keybinds.entries.associateBy { it.keybindName }
+
+ fun getById(value: Int): Keybinds? = byId[value]
+ fun getByName(name: String): Keybinds? = byName[name]
+ }
+}
class Keybinding @AWTThread constructor(val server: VRServer) : HotkeyListener {
val config: KeybindingsConfig = server.configManager.vrConfig.keybindings
init {
- if (currentPlatform != OperatingSystem.WINDOWS) {
- LogManager
- .info(
- "[Keybinding] Currently only supported on Windows. Keybindings will be disabled.",
- )
- } else {
+ if (currentPlatform == OperatingSystem.WINDOWS) {
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")
+ config.keybinds.forEach { (i, keybind) ->
+ JIntellitype.getInstance()
+ .registerHotKey(keybind.id, keybind.binding)
+ }
}
} catch (e: Throwable) {
LogManager
@@ -55,42 +53,97 @@ class Keybinding @AWTThread constructor(val server: VRServer) : HotkeyListener {
)
}
}
- }
+ if (currentPlatform == OperatingSystem.LINUX) {
+ val portalManager = PortalManager(SLIMEVR_IDENTIFIER)
+ val shortcutsList = Keybinds.entries.map {
+ ShortcutTuple(it.name, Shortcut(it.description, it.keybind).shortcut)
+ }.toMutableList()
- @AWTThread
- override fun onHotKey(identifier: Int) {
- when (identifier) {
- FULL_RESET -> server.scheduleResetTrackersFull(RESET_SOURCE_NAME, config.fullResetDelay)
+ val globalShortcutsHandler = portalManager.globalShortcutsRequest(shortcutsList)
+ Runtime.getRuntime().addShutdownHook(
+ Thread {
+ println("Closing connection")
+ globalShortcutsHandler.close()
+ },
+ )
- YAW_RESET -> server.scheduleResetTrackersYaw(RESET_SOURCE_NAME, config.yawResetDelay)
+ val onShortcut: (id: String) -> Unit = { shortcutId ->
+ val keybind = Keybinds.getByName(shortcutId)
+ if (keybind != null) {
+ val delay = config.keybinds[keybind]?.delay?.toLong() ?: 0L
+ when (keybind) {
+ Keybinds.FULL_RESET -> {
+ server.scheduleResetTrackersFull(keybind.keybindName, delay)
+ }
- MOUNTING_RESET -> server.scheduleResetTrackersMounting(
- RESET_SOURCE_NAME,
- config.mountingResetDelay,
- )
+ Keybinds.YAW_RESET -> {
+ server.scheduleResetTrackersYaw(keybind.keybindName, delay)
+ }
- FEET_MOUNTING_RESET -> server.scheduleResetTrackersMounting(
- RESET_SOURCE_NAME,
- config.feetMountingResetDelay,
- TrackerUtils.feetsBodyParts,
- )
+ Keybinds.MOUNTING_RESET -> {
+ server.scheduleResetTrackersMounting(
+ keybind.keybindName,
+ delay,
+ )
+ }
- PAUSE_TRACKING ->
- server
- .scheduleTogglePauseTracking(
- RESET_SOURCE_NAME,
- config.pauseTrackingDelay,
- )
+ Keybinds.FEET_MOUNTING_RESET -> {
+ server.scheduleResetTrackersMounting(
+ keybind.keybindName,
+ delay,
+ TrackerUtils.feetsBodyParts,
+ )
+ }
+
+ Keybinds.PAUSE_TRACKING -> {
+ server.scheduleTogglePauseTracking(
+ keybind.keybindName,
+ delay,
+ )
+ }
+ }
+ }
+ }
+
+ globalShortcutsHandler.onShortcutActivated = onShortcut
}
}
- companion object {
- private const val RESET_SOURCE_NAME = "Keybinding"
+ @AWTThread
+ override fun onHotKey(identifier: Int) {
+ val keybind = Keybinds.getById(identifier) ?: return
+ val delay = config.keybinds[keybind]?.delay?.toLong() ?: 0L
+ when (keybind) {
+ Keybinds.FULL_RESET -> {
+ server.scheduleResetTrackersFull(keybind.keybindName, delay)
+ }
+
+ Keybinds.YAW_RESET -> {
+ server.scheduleResetTrackersYaw(keybind.keybindName, delay)
+ }
- 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
+ Keybinds.MOUNTING_RESET -> {
+ server.scheduleResetTrackersMounting(
+ keybind.keybindName,
+ delay,
+ TrackerUtils.feetsBodyParts,
+ )
+ }
+
+ Keybinds.FEET_MOUNTING_RESET -> {
+ server.scheduleResetTrackersMounting(
+ keybind.keybindName,
+ delay,
+ TrackerUtils.feetsBodyParts,
+ )
+ }
+
+ Keybinds.PAUSE_TRACKING -> {
+ server.scheduleTogglePauseTracking(
+ keybind.keybindName,
+ delay,
+ )
+ }
+ }
}
}
diff --git a/server/core/src/main/java/dev/slimevr/VRServer.kt b/server/core/src/main/java/dev/slimevr/VRServer.kt
index 074631cc6e..4f5806fd86 100644
--- a/server/core/src/main/java/dev/slimevr/VRServer.kt
+++ b/server/core/src/main/java/dev/slimevr/VRServer.kt
@@ -11,6 +11,7 @@ import dev.slimevr.games.vrchat.VRCConfigHandler
import dev.slimevr.games.vrchat.VRCConfigHandlerStub
import dev.slimevr.games.vrchat.VRChatConfigManager
import dev.slimevr.guards.ServerGuards
+import dev.slimevr.keybind.KeybindHandler
import dev.slimevr.osc.OSCHandler
import dev.slimevr.osc.OSCRouter
import dev.slimevr.osc.VMCHandler
@@ -128,6 +129,8 @@ class VRServer @JvmOverloads constructor(
val networkProfileChecker: NetworkProfileChecker
+ val keybindHandler: KeybindHandler
+
val serverGuards = ServerGuards()
init {
@@ -144,6 +147,7 @@ class VRServer @JvmOverloads constructor(
vrcConfigManager = VRChatConfigManager(this, vrcConfigHandlerProvider(this))
networkProfileChecker = networkProfileProvider(this)
trackingChecklistManager = TrackingChecklistManager(this)
+ keybindHandler = KeybindHandler(this)
protocolAPI = ProtocolAPI(this)
val computedTrackers = humanPoseManager.computedTrackers
diff --git a/server/core/src/main/java/dev/slimevr/config/KeybindingsConfig.java b/server/core/src/main/java/dev/slimevr/config/KeybindingsConfig.java
deleted file mode 100644
index a3c1b0bfe3..0000000000
--- a/server/core/src/main/java/dev/slimevr/config/KeybindingsConfig.java
+++ /dev/null
@@ -1,88 +0,0 @@
-package dev.slimevr.config;
-
-public class KeybindingsConfig {
-
- private String fullResetBinding = "CTRL+ALT+SHIFT+Y";
-
- private String yawResetBinding = "CTRL+ALT+SHIFT+U";
-
- private String mountingResetBinding = "CTRL+ALT+SHIFT+I";
-
- private String feetMountingResetBinding = "CTRL+ALT+SHIFT+P";
-
- private String pauseTrackingBinding = "CTRL+ALT+SHIFT+O";
-
- private long fullResetDelay = 0L;
-
- private long yawResetDelay = 0L;
-
- private long mountingResetDelay = 0L;
-
- private long feetMountingResetDelay = 0L;
-
- private long pauseTrackingDelay = 0L;
-
-
- public KeybindingsConfig() {
- }
-
- public String getFullResetBinding() {
- return fullResetBinding;
- }
-
- public String getYawResetBinding() {
- return yawResetBinding;
- }
-
- public String getMountingResetBinding() {
- return mountingResetBinding;
- }
-
- public String getFeetMountingResetBinding() {
- return feetMountingResetBinding;
- }
-
- public String getPauseTrackingBinding() {
- return pauseTrackingBinding;
- }
-
- public long getFullResetDelay() {
- return fullResetDelay;
- }
-
- public void setFullResetDelay(long delay) {
- fullResetDelay = delay;
- }
-
- public long getYawResetDelay() {
- return yawResetDelay;
- }
-
- public void setYawResetDelay(long delay) {
- yawResetDelay = delay;
- }
-
- public long getMountingResetDelay() {
- return mountingResetDelay;
- }
-
- public void setMountingResetDelay(long delay) {
- mountingResetDelay = delay;
- }
-
- public long getFeetMountingResetDelay() {
- return feetMountingResetDelay;
- }
-
- public void setFeetMountingResetDelay(long delay) {
- feetMountingResetDelay = delay;
- }
-
- public long getPauseTrackingDelay() {
- return pauseTrackingDelay;
- }
-
- public void setPauseTrackingDelay(long delay) {
- pauseTrackingDelay = delay;
- }
-}
diff --git a/server/core/src/main/java/dev/slimevr/config/KeybindingsConfig.kt b/server/core/src/main/java/dev/slimevr/config/KeybindingsConfig.kt
new file mode 100644
index 0000000000..3505500096
--- /dev/null
+++ b/server/core/src/main/java/dev/slimevr/config/KeybindingsConfig.kt
@@ -0,0 +1,16 @@
+package dev.slimevr.config
+
+import dev.slimevr.Keybinds
+
+data class KeybindData(
+ var id: Int,
+ var name: String,
+ var binding: String,
+ var delay: Float,
+)
+
+class KeybindingsConfig {
+ val keybinds: MutableMap = Keybinds.entries
+ .associateWith { KeybindData(it.id, it.keybindName, it.keybind, 0f) }
+ .toMutableMap()
+}
diff --git a/server/core/src/main/java/dev/slimevr/keybind/KeybindHandler.kt b/server/core/src/main/java/dev/slimevr/keybind/KeybindHandler.kt
new file mode 100644
index 0000000000..6fbb4bb2cf
--- /dev/null
+++ b/server/core/src/main/java/dev/slimevr/keybind/KeybindHandler.kt
@@ -0,0 +1,55 @@
+package dev.slimevr.keybind
+
+import dev.slimevr.VRServer
+import dev.slimevr.config.KeybindingsConfig
+import solarxr_protocol.rpc.KeybindT
+import java.util.concurrent.CopyOnWriteArrayList
+
+class KeybindHandler(val vrServer: VRServer) {
+ private val listeners: MutableList = CopyOnWriteArrayList()
+ var keybinds: MutableList = mutableListOf()
+ var defaultKeybinds: MutableList = mutableListOf()
+
+ init {
+ createKeybinds()
+ }
+
+ fun addListener(listener: KeybindListener) {
+ this.listeners.add(listener)
+ }
+
+ fun removeListener(listener: KeybindListener) {
+ listeners.removeIf { listener == it }
+ }
+
+ private fun createKeybinds() {
+ keybinds.clear()
+ defaultKeybinds.clear()
+ vrServer.configManager.vrConfig.keybindings.keybinds.forEach { (i, keybind) ->
+ keybinds.add(
+ KeybindT().apply {
+ keybindId = keybind.id
+ keybindNameId = keybind.name
+ keybindValue = keybind.binding
+ keybindDelay = keybind.delay
+ },
+ )
+ }
+
+ val binds = KeybindingsConfig().keybinds
+ binds.forEach { (i, keybind) ->
+ defaultKeybinds.add(
+ KeybindT().apply {
+ keybindId = keybind.id
+ keybindNameId = keybind.name
+ keybindValue = keybind.binding
+ keybindDelay = keybind.delay
+ },
+ )
+ }
+ }
+
+ fun updateKeybinds() {
+ createKeybinds()
+ }
+}
diff --git a/server/core/src/main/java/dev/slimevr/keybind/KeybindListener.kt b/server/core/src/main/java/dev/slimevr/keybind/KeybindListener.kt
new file mode 100644
index 0000000000..0f4e9ade58
--- /dev/null
+++ b/server/core/src/main/java/dev/slimevr/keybind/KeybindListener.kt
@@ -0,0 +1,6 @@
+package dev.slimevr.keybind
+
+interface KeybindListener {
+ fun sendKeybind()
+ fun onKeybindUpdate()
+}
diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt
index 57be55772a..c0d9b4c8a7 100644
--- a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt
+++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt
@@ -11,6 +11,8 @@ import dev.slimevr.protocol.rpc.autobone.RPCAutoBoneHandler
import dev.slimevr.protocol.rpc.firmware.RPCFirmwareUpdateHandler
import dev.slimevr.protocol.rpc.games.vrchat.RPCVRChatHandler
import dev.slimevr.protocol.rpc.installinfo.RPCInstallInfoHandler
+import dev.slimevr.protocol.rpc.keybinds.RPCKeybindHandler
+import dev.slimevr.protocol.rpc.openuri.RPCOpenUriHandler
import dev.slimevr.protocol.rpc.reset.RPCResetHandler
import dev.slimevr.protocol.rpc.serial.RPCProvisioningHandler
import dev.slimevr.protocol.rpc.serial.RPCSerialHandler
@@ -54,6 +56,14 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler {
allprojects {
repositories {
// Use jcenter for resolving dependencies.
- // You can declare any Maven/Ivy/file repository here.
mavenCentral()
maven(url = "https://jitpack.io")
maven(url = "https://oss.sonatype.org/content/repositories/snapshots")
@@ -74,7 +73,7 @@ tasks.shadowJar {
exclude(dependency("com.fazecast:jSerialComm:.*"))
exclude(dependency("net.java.dev.jna:.*:.*"))
exclude(dependency("com.google.flatbuffers:flatbuffers-java:.*"))
-
+ exclude(dependency("com.github.HannahPadd:DbusGlobalShortcutsWayland:v0.1.0"))
exclude(project(":solarxr-protocol"))
}
archiveBaseName.set("slimevr")