mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-08-14 08:59:16 +02:00
6.0
This commit is contained in:
6
src/modules/mkb/base-mkb-handler.ts
Normal file → Executable file
6
src/modules/mkb/base-mkb-handler.ts
Normal file → Executable file
@@ -4,10 +4,11 @@ export abstract class MouseDataProvider {
|
||||
this.mkbHandler = handler;
|
||||
}
|
||||
|
||||
abstract init(): void;
|
||||
init() {};
|
||||
destroy() {};
|
||||
|
||||
abstract start(): void;
|
||||
abstract stop(): void;
|
||||
abstract destroy(): void;
|
||||
}
|
||||
|
||||
export abstract class MkbHandler {
|
||||
@@ -15,6 +16,7 @@ export abstract class MkbHandler {
|
||||
abstract start(): void;
|
||||
abstract stop(): void;
|
||||
abstract destroy(): void;
|
||||
abstract toggle(force: boolean): void;
|
||||
abstract handleMouseMove(data: MkbMouseMove): void;
|
||||
abstract handleMouseClick(data: MkbMouseClick): void;
|
||||
abstract handleMouseWheel(data: MkbMouseWheel): boolean;
|
||||
|
113
src/modules/mkb/key-helper.ts
Normal file → Executable file
113
src/modules/mkb/key-helper.ts
Normal file → Executable file
@@ -1,8 +1,36 @@
|
||||
import { MouseButtonCode, WheelCode } from "@enums/mkb";
|
||||
import { MouseButtonCode, WheelCode, type KeyCode } from "@/enums/mkb";
|
||||
|
||||
export const enum KeyModifier {
|
||||
CTRL = 1,
|
||||
SHIFT = 2,
|
||||
ALT = 4,
|
||||
};
|
||||
|
||||
export type KeyEventInfo = {
|
||||
code: KeyCode | MouseButtonCode | WheelCode;
|
||||
modifiers?: number;
|
||||
};
|
||||
|
||||
export class KeyHelper {
|
||||
static #NON_PRINTABLE_KEYS = {
|
||||
'Backquote': '`',
|
||||
private static readonly NON_PRINTABLE_KEYS = {
|
||||
Backquote: '`',
|
||||
Minus: '-',
|
||||
Equal: '=',
|
||||
BracketLeft: '[',
|
||||
BracketRight: ']',
|
||||
Backslash: '\\',
|
||||
Semicolon: ';',
|
||||
Quote: '\'',
|
||||
Comma: ',',
|
||||
Period: '.',
|
||||
Slash: '/',
|
||||
|
||||
NumpadMultiply: 'Numpad *',
|
||||
NumpadAdd: 'Numpad +',
|
||||
NumpadSubtract: 'Numpad -',
|
||||
NumpadDecimal: 'Numpad .',
|
||||
NumpadDivide: 'Numpad /',
|
||||
NumpadEqual: 'Numpad =',
|
||||
|
||||
// Mouse buttons
|
||||
[MouseButtonCode.LEFT_CLICK]: 'Left Click',
|
||||
@@ -15,12 +43,19 @@ export class KeyHelper {
|
||||
[WheelCode.SCROLL_RIGHT]: 'Scroll Right',
|
||||
};
|
||||
|
||||
static getKeyFromEvent(e: Event) {
|
||||
let code;
|
||||
let name;
|
||||
static getKeyFromEvent(e: Event): KeyEventInfo | null {
|
||||
let code: KeyEventInfo['code'] | null = null;
|
||||
let modifiers;
|
||||
|
||||
if (e instanceof KeyboardEvent) {
|
||||
code = e.code || e.key;
|
||||
code = (e.code || e.key) as KeyCode;
|
||||
|
||||
// Modifiers
|
||||
modifiers = 0;
|
||||
modifiers ^= e.ctrlKey ? KeyModifier.CTRL : 0;
|
||||
modifiers ^= e.shiftKey ? KeyModifier.SHIFT : 0;
|
||||
modifiers ^= e.altKey ? KeyModifier.ALT : 0;
|
||||
|
||||
} else if (e instanceof WheelEvent) {
|
||||
if (e.deltaY < 0) {
|
||||
code = WheelCode.SCROLL_UP;
|
||||
@@ -32,20 +67,47 @@ export class KeyHelper {
|
||||
code = WheelCode.SCROLL_RIGHT;
|
||||
}
|
||||
} else if (e instanceof MouseEvent) {
|
||||
code = 'Mouse' + e.button;
|
||||
code = 'Mouse' + e.button as MouseButtonCode;
|
||||
}
|
||||
|
||||
if (code) {
|
||||
name = KeyHelper.codeToKeyName(code);
|
||||
const results: KeyEventInfo = { code };
|
||||
if (modifiers) {
|
||||
results.modifiers = modifiers;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
return code ? {code, name} : null;
|
||||
return null;
|
||||
}
|
||||
|
||||
static codeToKeyName(code: string) {
|
||||
return (
|
||||
// @ts-ignore
|
||||
KeyHelper.#NON_PRINTABLE_KEYS[code]
|
||||
static getFullKeyCodeFromEvent(e: KeyboardEvent): string {
|
||||
const key = KeyHelper.getKeyFromEvent(e);
|
||||
return key ? `${key.code}:${key.modifiers || 0}` : '';
|
||||
}
|
||||
|
||||
static parseFullKeyCode(str: string | undefined | null): KeyEventInfo | null {
|
||||
if (!str) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tmp = str.split(':');
|
||||
|
||||
const code = tmp[0] as KeyEventInfo['code'];
|
||||
const modifiers = parseInt(tmp[1]);
|
||||
|
||||
return {
|
||||
code,
|
||||
modifiers,
|
||||
} as KeyEventInfo;
|
||||
}
|
||||
|
||||
static codeToKeyName(key: KeyEventInfo): string {
|
||||
const { code, modifiers } = key;
|
||||
|
||||
const text = [(
|
||||
KeyHelper.NON_PRINTABLE_KEYS[code as keyof typeof KeyHelper.NON_PRINTABLE_KEYS]
|
||||
||
|
||||
(code.startsWith('Key') && code.substring(3))
|
||||
||
|
||||
@@ -62,6 +124,27 @@ export class KeyHelper {
|
||||
(code.endsWith('Right') && ('Right ' + code.replace('Right', '')))
|
||||
||
|
||||
code
|
||||
);
|
||||
)];
|
||||
|
||||
if (modifiers && modifiers !== 0) {
|
||||
if (!code.startsWith('Control') && !code.startsWith('Shift') && !code.startsWith('Alt')) {
|
||||
// Shift
|
||||
if (modifiers & KeyModifier.SHIFT) {
|
||||
text.unshift('Shift');
|
||||
}
|
||||
|
||||
// Alt
|
||||
if (modifiers & KeyModifier.ALT) {
|
||||
text.unshift('Alt');
|
||||
}
|
||||
|
||||
// Ctrl
|
||||
if (modifiers & KeyModifier.CTRL) {
|
||||
text.unshift('Ctrl');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return text.join(' + ');
|
||||
}
|
||||
}
|
||||
|
40
src/modules/mkb/keyboard-shortcut-handler.ts
Executable file
40
src/modules/mkb/keyboard-shortcut-handler.ts
Executable file
@@ -0,0 +1,40 @@
|
||||
import { ShortcutHandler } from "@/utils/shortcut-handler";
|
||||
import { KeyHelper } from "./key-helper";
|
||||
|
||||
export class KeyboardShortcutHandler {
|
||||
private static instance: KeyboardShortcutHandler;
|
||||
public static getInstance = () => KeyboardShortcutHandler.instance ?? (KeyboardShortcutHandler.instance = new KeyboardShortcutHandler());
|
||||
|
||||
start() {
|
||||
window.addEventListener('keydown', this.onKeyDown);
|
||||
}
|
||||
|
||||
stop() {
|
||||
window.removeEventListener('keydown', this.onKeyDown);
|
||||
}
|
||||
|
||||
onKeyDown = (e: KeyboardEvent) => {
|
||||
// Don't run when the stream is not being focused
|
||||
if (window.BX_STREAM_SETTINGS.xCloudPollingMode !== 'none') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't activate repeated key
|
||||
if (e.repeat) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check unknown key
|
||||
const fullKeyCode = KeyHelper.getFullKeyCodeFromEvent(e);
|
||||
if (!fullKeyCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const action = window.BX_STREAM_SETTINGS.keyboardShortcuts?.[fullKeyCode];
|
||||
if (action) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
ShortcutHandler.runAction(action);
|
||||
}
|
||||
}
|
||||
}
|
551
src/modules/mkb/mkb-handler.ts
Normal file → Executable file
551
src/modules/mkb/mkb-handler.ts
Normal file → Executable file
@@ -1,24 +1,21 @@
|
||||
import { isFullVersion } from "@macros/build" with {type: "macro"};
|
||||
|
||||
import { MkbPreset } from "./mkb-preset";
|
||||
import { GamepadKey, MkbPresetKey, GamepadStick, MouseMapTo, WheelCode } from "@enums/mkb";
|
||||
import { createButton, ButtonStyle, CE } from "@utils/html";
|
||||
import { MkbPresetKey, MouseConstant, MouseMapTo, WheelCode } from "@/enums/mkb";
|
||||
import { BxEvent } from "@utils/bx-event";
|
||||
import { Toast } from "@utils/toast";
|
||||
import { t } from "@utils/translation";
|
||||
import { KeyHelper } from "./key-helper";
|
||||
import type { MkbStoredPreset } from "@/types/mkb";
|
||||
import { AppInterface, STATES } from "@utils/global";
|
||||
import { UserAgent } from "@utils/user-agent";
|
||||
import { BxLogger } from "@utils/bx-logger";
|
||||
import { PointerClient } from "./pointer-client";
|
||||
import { NativeMkbHandler } from "./native-mkb-handler";
|
||||
import { MkbHandler, MouseDataProvider } from "./base-mkb-handler";
|
||||
import { SettingsNavigationDialog } from "../ui/dialog/settings-dialog";
|
||||
import { NavigationDialogManager } from "../ui/dialog/navigation-dialog";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { MkbPresetsDb } from "@/utils/local-db/mkb-presets-db";
|
||||
import { GamepadKey, GamepadStick } from "@/enums/gamepad";
|
||||
import { MkbPopup } from "./mkb-popup";
|
||||
import type { MkbConvertedPresetData } from "@/types/presets";
|
||||
|
||||
const PointerToMouseButton = {
|
||||
1: 0,
|
||||
@@ -26,79 +23,74 @@ const PointerToMouseButton = {
|
||||
4: 1,
|
||||
}
|
||||
|
||||
export const VIRTUAL_GAMEPAD_ID = 'Xbox 360 Controller';
|
||||
export const VIRTUAL_GAMEPAD_ID = 'Better xCloud Virtual Controller';
|
||||
|
||||
class WebSocketMouseDataProvider extends MouseDataProvider {
|
||||
#pointerClient: PointerClient | undefined
|
||||
#connected = false
|
||||
private pointerClient: PointerClient | undefined
|
||||
private isConnected = false
|
||||
|
||||
init(): void {
|
||||
this.#pointerClient = PointerClient.getInstance();
|
||||
this.#connected = false;
|
||||
this.pointerClient = PointerClient.getInstance();
|
||||
this.isConnected = false;
|
||||
try {
|
||||
this.#pointerClient.start(STATES.pointerServerPort, this.mkbHandler);
|
||||
this.#connected = true;
|
||||
this.pointerClient.start(STATES.pointerServerPort, this.mkbHandler);
|
||||
this.isConnected = true;
|
||||
} catch (e) {
|
||||
Toast.show('Cannot enable Mouse & Keyboard feature');
|
||||
}
|
||||
}
|
||||
|
||||
start(): void {
|
||||
this.#connected && AppInterface.requestPointerCapture();
|
||||
this.isConnected && AppInterface.requestPointerCapture();
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.#connected && AppInterface.releasePointerCapture();
|
||||
this.isConnected && AppInterface.releasePointerCapture();
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.#connected && this.#pointerClient?.stop();
|
||||
this.isConnected && this.pointerClient?.stop();
|
||||
}
|
||||
}
|
||||
|
||||
class PointerLockMouseDataProvider extends MouseDataProvider {
|
||||
init(): void {}
|
||||
|
||||
start(): void {
|
||||
window.addEventListener('mousemove', this.#onMouseMoveEvent);
|
||||
window.addEventListener('mousedown', this.#onMouseEvent);
|
||||
window.addEventListener('mouseup', this.#onMouseEvent);
|
||||
window.addEventListener('wheel', this.#onWheelEvent, {passive: false});
|
||||
window.addEventListener('contextmenu', this.#disableContextMenu);
|
||||
start() {
|
||||
window.addEventListener('mousemove', this.onMouseMoveEvent);
|
||||
window.addEventListener('mousedown', this.onMouseEvent);
|
||||
window.addEventListener('mouseup', this.onMouseEvent);
|
||||
window.addEventListener('wheel', this.onWheelEvent, { passive: false });
|
||||
window.addEventListener('contextmenu', this.disableContextMenu);
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
stop() {
|
||||
document.pointerLockElement && document.exitPointerLock();
|
||||
|
||||
window.removeEventListener('mousemove', this.#onMouseMoveEvent);
|
||||
window.removeEventListener('mousedown', this.#onMouseEvent);
|
||||
window.removeEventListener('mouseup', this.#onMouseEvent);
|
||||
window.removeEventListener('wheel', this.#onWheelEvent);
|
||||
window.removeEventListener('contextmenu', this.#disableContextMenu);
|
||||
window.removeEventListener('mousemove', this.onMouseMoveEvent);
|
||||
window.removeEventListener('mousedown', this.onMouseEvent);
|
||||
window.removeEventListener('mouseup', this.onMouseEvent);
|
||||
window.removeEventListener('wheel', this.onWheelEvent);
|
||||
window.removeEventListener('contextmenu', this.disableContextMenu);
|
||||
}
|
||||
|
||||
destroy(): void {}
|
||||
|
||||
#onMouseMoveEvent = (e: MouseEvent) => {
|
||||
private onMouseMoveEvent = (e: MouseEvent) => {
|
||||
this.mkbHandler.handleMouseMove({
|
||||
movementX: e.movementX,
|
||||
movementY: e.movementY,
|
||||
});
|
||||
}
|
||||
|
||||
#onMouseEvent = (e: MouseEvent) => {
|
||||
private onMouseEvent = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const isMouseDown = e.type === 'mousedown';
|
||||
const data: MkbMouseClick = {
|
||||
mouseButton: e.button,
|
||||
pressed: isMouseDown,
|
||||
pressed: e.type === 'mousedown',
|
||||
};
|
||||
|
||||
this.mkbHandler.handleMouseClick(data);
|
||||
}
|
||||
|
||||
#onWheelEvent = (e: WheelEvent) => {
|
||||
private onWheelEvent = (e: WheelEvent) => {
|
||||
const key = KeyHelper.getKeyFromEvent(e);
|
||||
if (!key) {
|
||||
return;
|
||||
@@ -114,7 +106,7 @@ class PointerLockMouseDataProvider extends MouseDataProvider {
|
||||
}
|
||||
}
|
||||
|
||||
#disableContextMenu = (e: Event) => e.preventDefault();
|
||||
private disableContextMenu = (e: Event) => e.preventDefault();
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -122,80 +114,95 @@ This class uses some code from Yuzu emulator to handle mouse's movements
|
||||
Source: https://github.com/yuzu-emu/yuzu-mainline/blob/master/src/input_common/drivers/mouse.cpp
|
||||
*/
|
||||
export class EmulatedMkbHandler extends MkbHandler {
|
||||
private static instance: EmulatedMkbHandler;
|
||||
public static getInstance = () => EmulatedMkbHandler.instance ?? (EmulatedMkbHandler.instance = new EmulatedMkbHandler());
|
||||
private static instance: EmulatedMkbHandler | null | undefined;
|
||||
public static getInstance(): typeof EmulatedMkbHandler['instance'] {
|
||||
if (typeof EmulatedMkbHandler.instance === 'undefined') {
|
||||
if (EmulatedMkbHandler.isAllowed()) {
|
||||
EmulatedMkbHandler.instance = new EmulatedMkbHandler();
|
||||
} else {
|
||||
EmulatedMkbHandler.instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
return EmulatedMkbHandler.instance;
|
||||
}
|
||||
|
||||
private static readonly LOG_TAG = 'EmulatedMkbHandler';
|
||||
|
||||
#CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET);
|
||||
static isAllowed() {
|
||||
return getPref(PrefKey.MKB_ENABLED) && (AppInterface || !UserAgent.isMobile());
|
||||
}
|
||||
|
||||
static readonly DEFAULT_PANNING_SENSITIVITY = 0.0010;
|
||||
static readonly DEFAULT_DEADZONE_COUNTERWEIGHT = 0.01;
|
||||
static readonly MAXIMUM_STICK_RANGE = 1.1;
|
||||
private PRESET!: MkbConvertedPresetData | null;
|
||||
private VIRTUAL_GAMEPAD = {
|
||||
id: VIRTUAL_GAMEPAD_ID,
|
||||
index: 0,
|
||||
connected: false,
|
||||
hapticActuators: null,
|
||||
mapping: 'standard',
|
||||
|
||||
#VIRTUAL_GAMEPAD = {
|
||||
id: VIRTUAL_GAMEPAD_ID,
|
||||
index: 3,
|
||||
connected: false,
|
||||
hapticActuators: null,
|
||||
mapping: 'standard',
|
||||
axes: [0, 0, 0, 0],
|
||||
buttons: new Array(17).fill(null).map(() => ({pressed: false, value: 0})),
|
||||
timestamp: performance.now(),
|
||||
|
||||
axes: [0, 0, 0, 0],
|
||||
buttons: new Array(17).fill(null).map(() => ({pressed: false, value: 0})),
|
||||
timestamp: performance.now(),
|
||||
vibrationActuator: null,
|
||||
};
|
||||
private nativeGetGamepads: Navigator['getGamepads'];
|
||||
|
||||
vibrationActuator: null,
|
||||
};
|
||||
#nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator);
|
||||
private initialized = false;
|
||||
private enabled = false;
|
||||
private mouseDataProvider: MouseDataProvider | undefined;
|
||||
private isPolling = false;
|
||||
|
||||
#enabled = false;
|
||||
#mouseDataProvider: MouseDataProvider | undefined;
|
||||
#isPolling = false;
|
||||
private prevWheelCode = null;
|
||||
private wheelStoppedTimeoutId: number | null = null;
|
||||
|
||||
#prevWheelCode = null;
|
||||
#wheelStoppedTimeout?: number | null;
|
||||
private detectMouseStoppedTimeoutId: number | null = null;
|
||||
|
||||
#detectMouseStoppedTimeout?: number | null;
|
||||
private escKeyDownTime: number = -1;
|
||||
|
||||
#$message?: HTMLElement;
|
||||
private LEFT_STICK_X: GamepadKey[] = [];
|
||||
private LEFT_STICK_Y: GamepadKey[] = [];
|
||||
private RIGHT_STICK_X: GamepadKey[] = [];
|
||||
private RIGHT_STICK_Y: GamepadKey[] = [];
|
||||
|
||||
#escKeyDownTime: number = -1;
|
||||
private popup: MkbPopup;
|
||||
|
||||
#STICK_MAP: {[key in GamepadKey]?: [GamepadKey[], number, number]};
|
||||
#LEFT_STICK_X: GamepadKey[] = [];
|
||||
#LEFT_STICK_Y: GamepadKey[] = [];
|
||||
#RIGHT_STICK_X: GamepadKey[] = [];
|
||||
#RIGHT_STICK_Y: GamepadKey[] = [];
|
||||
private STICK_MAP: {[key in GamepadKey]?: [GamepadKey[], number, number]} = {
|
||||
[GamepadKey.LS_LEFT]: [this.LEFT_STICK_X, 0, -1],
|
||||
[GamepadKey.LS_RIGHT]: [this.LEFT_STICK_X, 0, 1],
|
||||
[GamepadKey.LS_UP]: [this.LEFT_STICK_Y, 1, -1],
|
||||
[GamepadKey.LS_DOWN]: [this.LEFT_STICK_Y, 1, 1],
|
||||
|
||||
[GamepadKey.RS_LEFT]: [this.RIGHT_STICK_X, 2, -1],
|
||||
[GamepadKey.RS_RIGHT]: [this.RIGHT_STICK_X, 2, 1],
|
||||
[GamepadKey.RS_UP]: [this.RIGHT_STICK_Y, 3, -1],
|
||||
[GamepadKey.RS_DOWN]: [this.RIGHT_STICK_Y, 3, 1],
|
||||
};
|
||||
|
||||
private constructor() {
|
||||
super();
|
||||
BxLogger.info(EmulatedMkbHandler.LOG_TAG, 'constructor()');
|
||||
|
||||
this.#STICK_MAP = {
|
||||
[GamepadKey.LS_LEFT]: [this.#LEFT_STICK_X, 0, -1],
|
||||
[GamepadKey.LS_RIGHT]: [this.#LEFT_STICK_X, 0, 1],
|
||||
[GamepadKey.LS_UP]: [this.#LEFT_STICK_Y, 1, -1],
|
||||
[GamepadKey.LS_DOWN]: [this.#LEFT_STICK_Y, 1, 1],
|
||||
this.nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator);
|
||||
|
||||
[GamepadKey.RS_LEFT]: [this.#RIGHT_STICK_X, 2, -1],
|
||||
[GamepadKey.RS_RIGHT]: [this.#RIGHT_STICK_X, 2, 1],
|
||||
[GamepadKey.RS_UP]: [this.#RIGHT_STICK_Y, 3, -1],
|
||||
[GamepadKey.RS_DOWN]: [this.#RIGHT_STICK_Y, 3, 1],
|
||||
};
|
||||
this.popup = MkbPopup.getInstance();
|
||||
this.popup.attachMkbHandler(this);
|
||||
}
|
||||
|
||||
isEnabled = () => this.#enabled;
|
||||
isEnabled = () => this.enabled;
|
||||
|
||||
#patchedGetGamepads = () => {
|
||||
const gamepads = this.#nativeGetGamepads() || [];
|
||||
(gamepads as any)[this.#VIRTUAL_GAMEPAD.index] = this.#VIRTUAL_GAMEPAD;
|
||||
private patchedGetGamepads = () => {
|
||||
const gamepads = (this.nativeGetGamepads() || []) as any;
|
||||
|
||||
gamepads[this.VIRTUAL_GAMEPAD.index] = this.VIRTUAL_GAMEPAD;
|
||||
return gamepads;
|
||||
}
|
||||
|
||||
#getVirtualGamepad = () => this.#VIRTUAL_GAMEPAD;
|
||||
private getVirtualGamepad = () => this.VIRTUAL_GAMEPAD;
|
||||
|
||||
#updateStick(stick: GamepadStick, x: number, y: number) {
|
||||
const virtualGamepad = this.#getVirtualGamepad();
|
||||
private updateStick(stick: GamepadStick, x: number, y: number) {
|
||||
const virtualGamepad = this.getVirtualGamepad();
|
||||
virtualGamepad.axes[stick * 2] = x;
|
||||
virtualGamepad.axes[stick * 2 + 1] = y;
|
||||
|
||||
@@ -212,10 +219,10 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
}
|
||||
*/
|
||||
|
||||
#vectorLength = (x: number, y: number): number => Math.sqrt(x ** 2 + y ** 2);
|
||||
private vectorLength = (x: number, y: number): number => Math.sqrt(x ** 2 + y ** 2);
|
||||
|
||||
#resetGamepad = () => {
|
||||
const gamepad = this.#getVirtualGamepad();
|
||||
private resetGamepad() {
|
||||
const gamepad = this.getVirtualGamepad();
|
||||
|
||||
// Reset axes
|
||||
gamepad.axes = [0, 0, 0, 0];
|
||||
@@ -229,11 +236,11 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
gamepad.timestamp = performance.now();
|
||||
}
|
||||
|
||||
#pressButton = (buttonIndex: GamepadKey, pressed: boolean) => {
|
||||
const virtualGamepad = this.#getVirtualGamepad();
|
||||
private pressButton(buttonIndex: GamepadKey, pressed: boolean) {
|
||||
const virtualGamepad = this.getVirtualGamepad();
|
||||
|
||||
if (buttonIndex >= 100) {
|
||||
let [valueArr, axisIndex] = this.#STICK_MAP[buttonIndex]!;
|
||||
let [valueArr, axisIndex] = this.STICK_MAP[buttonIndex]!;
|
||||
valueArr = valueArr as number[];
|
||||
axisIndex = axisIndex as number;
|
||||
|
||||
@@ -249,7 +256,7 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
let value;
|
||||
if (valueArr.length) {
|
||||
// Get value of the last key of the axis
|
||||
value = this.#STICK_MAP[valueArr[valueArr.length - 1]]![2] as number;
|
||||
value = this.STICK_MAP[valueArr[valueArr.length - 1]]![2] as number;
|
||||
} else {
|
||||
value = 0;
|
||||
}
|
||||
@@ -263,41 +270,35 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
virtualGamepad.timestamp = performance.now();
|
||||
}
|
||||
|
||||
#onKeyboardEvent = (e: KeyboardEvent) => {
|
||||
private onKeyboardEvent = (e: KeyboardEvent) => {
|
||||
const isKeyDown = e.type === 'keydown';
|
||||
|
||||
// Toggle MKB feature
|
||||
if (e.code === 'F8') {
|
||||
if (!isKeyDown) {
|
||||
e.preventDefault();
|
||||
this.toggle();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Hijack the Esc button
|
||||
if (e.code === 'Escape') {
|
||||
e.preventDefault();
|
||||
|
||||
// Hold the Esc for 1 second to disable MKB
|
||||
if (this.#enabled && isKeyDown) {
|
||||
if (this.#escKeyDownTime === -1) {
|
||||
this.#escKeyDownTime = performance.now();
|
||||
} else if (performance.now() - this.#escKeyDownTime >= 1000) {
|
||||
if (this.enabled && isKeyDown) {
|
||||
if (this.escKeyDownTime === -1) {
|
||||
this.escKeyDownTime = performance.now();
|
||||
} else if (performance.now() - this.escKeyDownTime >= 1000) {
|
||||
this.stop();
|
||||
}
|
||||
} else {
|
||||
this.#escKeyDownTime = -1;
|
||||
this.escKeyDownTime = -1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.#isPolling) {
|
||||
if (!this.isPolling || !this.PRESET) {
|
||||
return;
|
||||
}
|
||||
|
||||
const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[e.code || e.key]!;
|
||||
if (window.BX_STREAM_SETTINGS.xCloudPollingMode !== 'none') {
|
||||
return;
|
||||
}
|
||||
|
||||
const buttonIndex = this.PRESET.mapping[e.code || e.key]!;
|
||||
if (typeof buttonIndex === 'undefined') {
|
||||
return;
|
||||
}
|
||||
@@ -308,19 +309,23 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
this.#pressButton(buttonIndex, isKeyDown);
|
||||
this.pressButton(buttonIndex, isKeyDown);
|
||||
}
|
||||
|
||||
#onMouseStopped = () => {
|
||||
private onMouseStopped = () => {
|
||||
// Reset stick position
|
||||
this.#detectMouseStoppedTimeout = null;
|
||||
this.detectMouseStoppedTimeoutId = null;
|
||||
|
||||
const mouseMapTo = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_MAP_TO];
|
||||
if (!this.PRESET) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mouseMapTo = this.PRESET.mouse[MkbPresetKey.MOUSE_MAP_TO];
|
||||
const analog = mouseMapTo === MouseMapTo.LS ? GamepadStick.LEFT : GamepadStick.RIGHT;
|
||||
this.#updateStick(analog, 0, 0);
|
||||
this.updateStick(analog, 0, 0);
|
||||
}
|
||||
|
||||
handleMouseClick = (data: MkbMouseClick) => {
|
||||
handleMouseClick(data: MkbMouseClick) {
|
||||
let mouseButton;
|
||||
if (typeof data.mouseButton !== 'undefined') {
|
||||
mouseButton = data.mouseButton;
|
||||
@@ -331,51 +336,54 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
const keyCode = 'Mouse' + mouseButton;
|
||||
const key = {
|
||||
code: keyCode,
|
||||
name: KeyHelper.codeToKeyName(keyCode),
|
||||
};
|
||||
|
||||
if (!key.name) {
|
||||
if (!this.PRESET) {
|
||||
return;
|
||||
}
|
||||
|
||||
const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]!;
|
||||
const buttonIndex = this.PRESET.mapping[key.code]!;
|
||||
if (typeof buttonIndex === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#pressButton(buttonIndex, data.pressed);
|
||||
this.pressButton(buttonIndex, data.pressed);
|
||||
}
|
||||
|
||||
handleMouseMove = (data: MkbMouseMove) => {
|
||||
handleMouseMove(data: MkbMouseMove) {
|
||||
const preset = this.PRESET;
|
||||
if (!preset) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: optimize this
|
||||
const mouseMapTo = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_MAP_TO];
|
||||
const mouseMapTo = preset.mouse[MkbPresetKey.MOUSE_MAP_TO];
|
||||
if (mouseMapTo === MouseMapTo.OFF) {
|
||||
// Ignore mouse movements
|
||||
return;
|
||||
}
|
||||
|
||||
this.#detectMouseStoppedTimeout && clearTimeout(this.#detectMouseStoppedTimeout);
|
||||
this.#detectMouseStoppedTimeout = window.setTimeout(this.#onMouseStopped.bind(this), 50);
|
||||
this.detectMouseStoppedTimeoutId && clearTimeout(this.detectMouseStoppedTimeoutId);
|
||||
this.detectMouseStoppedTimeoutId = window.setTimeout(this.onMouseStopped, 50);
|
||||
|
||||
const deadzoneCounterweight = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT];
|
||||
const deadzoneCounterweight = preset.mouse[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT];
|
||||
|
||||
let x = data.movementX * this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_SENSITIVITY_X];
|
||||
let y = data.movementY * this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_SENSITIVITY_Y];
|
||||
let x = data.movementX * preset.mouse[MkbPresetKey.MOUSE_SENSITIVITY_X];
|
||||
let y = data.movementY * preset.mouse[MkbPresetKey.MOUSE_SENSITIVITY_Y];
|
||||
|
||||
let length = this.#vectorLength(x, y);
|
||||
let length = this.vectorLength(x, y);
|
||||
if (length !== 0 && length < deadzoneCounterweight) {
|
||||
x *= deadzoneCounterweight / length;
|
||||
y *= deadzoneCounterweight / length;
|
||||
} else if (length > EmulatedMkbHandler.MAXIMUM_STICK_RANGE) {
|
||||
x *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length;
|
||||
y *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length;
|
||||
} else if (length > MouseConstant.MAXIMUM_STICK_RANGE) {
|
||||
x *= MouseConstant.MAXIMUM_STICK_RANGE / length;
|
||||
y *= MouseConstant.MAXIMUM_STICK_RANGE / length;
|
||||
}
|
||||
|
||||
const analog = mouseMapTo === MouseMapTo.LS ? GamepadStick.LEFT : GamepadStick.RIGHT;
|
||||
this.#updateStick(analog, x, y);
|
||||
this.updateStick(analog, x, y);
|
||||
}
|
||||
|
||||
handleMouseWheel = (data: MkbMouseWheel): boolean => {
|
||||
handleMouseWheel(data: MkbMouseWheel): boolean {
|
||||
let code = '';
|
||||
if (data.vertical < 0) {
|
||||
code = WheelCode.SCROLL_UP;
|
||||
@@ -391,136 +399,69 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.PRESET) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const key = {
|
||||
code: code,
|
||||
name: KeyHelper.codeToKeyName(code),
|
||||
};
|
||||
|
||||
const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]!;
|
||||
const buttonIndex = this.PRESET.mapping[key.code]!;
|
||||
if (typeof buttonIndex === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.#prevWheelCode === null || this.#prevWheelCode === key.code) {
|
||||
this.#wheelStoppedTimeout && clearTimeout(this.#wheelStoppedTimeout);
|
||||
this.#pressButton(buttonIndex, true);
|
||||
if (this.prevWheelCode === null || this.prevWheelCode === key.code) {
|
||||
this.wheelStoppedTimeoutId && clearTimeout(this.wheelStoppedTimeoutId);
|
||||
this.pressButton(buttonIndex, true);
|
||||
}
|
||||
|
||||
this.#wheelStoppedTimeout = window.setTimeout(() => {
|
||||
this.#prevWheelCode = null;
|
||||
this.#pressButton(buttonIndex, false);
|
||||
this.wheelStoppedTimeoutId = window.setTimeout(() => {
|
||||
this.prevWheelCode = null;
|
||||
this.pressButton(buttonIndex, false);
|
||||
}, 20);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
toggle = (force?: boolean) => {
|
||||
if (typeof force !== 'undefined') {
|
||||
this.#enabled = force;
|
||||
} else {
|
||||
this.#enabled = !this.#enabled;
|
||||
toggle(force?: boolean) {
|
||||
if (!this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.#enabled) {
|
||||
if (typeof force !== 'undefined') {
|
||||
this.enabled = force;
|
||||
} else {
|
||||
this.enabled = !this.enabled;
|
||||
}
|
||||
|
||||
if (this.enabled) {
|
||||
document.body.requestPointerLock();
|
||||
} else {
|
||||
document.pointerLockElement && document.exitPointerLock();
|
||||
}
|
||||
}
|
||||
|
||||
#getCurrentPreset = (): Promise<MkbStoredPreset> => {
|
||||
return new Promise(resolve => {
|
||||
const presetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
|
||||
MkbPresetsDb.getInstance().getPreset(presetId).then((preset: MkbStoredPreset) => {
|
||||
resolve(preset);
|
||||
});
|
||||
});
|
||||
refreshPresetData() {
|
||||
this.PRESET = window.BX_STREAM_SETTINGS.mkbPreset;
|
||||
this.resetGamepad();
|
||||
}
|
||||
|
||||
refreshPresetData = () => {
|
||||
this.#getCurrentPreset().then((preset: MkbStoredPreset) => {
|
||||
this.#CURRENT_PRESET_DATA = MkbPreset.convert(preset ? preset.data : MkbPreset.DEFAULT_PRESET);
|
||||
this.#resetGamepad();
|
||||
});
|
||||
waitForMouseData(showPopup: boolean) {
|
||||
this.popup.toggleVisibility(showPopup);
|
||||
}
|
||||
|
||||
waitForMouseData = (wait: boolean) => {
|
||||
this.#$message && this.#$message.classList.toggle('bx-gone', !wait);
|
||||
private onPollingModeChanged = (e: Event) => {
|
||||
const move = window.BX_STREAM_SETTINGS.xCloudPollingMode !== 'none';
|
||||
this.popup.moveOffscreen(move);
|
||||
}
|
||||
|
||||
#onPollingModeChanged = (e: Event) => {
|
||||
if (!this.#$message) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mode = (e as any).mode;
|
||||
if (mode === 'none') {
|
||||
this.#$message.classList.remove('bx-offscreen');
|
||||
} else {
|
||||
this.#$message.classList.add('bx-offscreen');
|
||||
}
|
||||
}
|
||||
|
||||
#onDialogShown = () => {
|
||||
private onDialogShown = () => {
|
||||
document.pointerLockElement && document.exitPointerLock();
|
||||
}
|
||||
|
||||
#initMessage = () => {
|
||||
if (!this.#$message) {
|
||||
this.#$message = CE('div', {'class': 'bx-mkb-pointer-lock-msg bx-gone'},
|
||||
CE('div', {},
|
||||
CE('p', {}, t('virtual-controller')),
|
||||
CE('p', {}, t('press-key-to-toggle-mkb', {key: 'F8'})),
|
||||
),
|
||||
|
||||
CE('div', {'data-type': 'virtual'},
|
||||
createButton({
|
||||
style: ButtonStyle.PRIMARY | ButtonStyle.TALL | ButtonStyle.FULL_WIDTH,
|
||||
label: t('activate'),
|
||||
onClick: ((e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.toggle(true);
|
||||
}).bind(this),
|
||||
}),
|
||||
|
||||
CE('div', {},
|
||||
createButton({
|
||||
label: t('ignore'),
|
||||
style: ButtonStyle.GHOST,
|
||||
onClick: e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.toggle(false);
|
||||
this.waitForMouseData(false);
|
||||
},
|
||||
}),
|
||||
|
||||
createButton({
|
||||
label: t('edit'),
|
||||
onClick: e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Show Settings dialog & focus the MKB tab
|
||||
const dialog = SettingsNavigationDialog.getInstance();
|
||||
dialog.focusTab('mkb');
|
||||
NavigationDialogManager.getInstance().show(dialog);
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.#$message.isConnected) {
|
||||
document.documentElement.appendChild(this.#$message);
|
||||
}
|
||||
}
|
||||
|
||||
#onPointerLockChange = () => {
|
||||
private onPointerLockChange = () => {
|
||||
if (document.pointerLockElement) {
|
||||
this.start();
|
||||
} else {
|
||||
@@ -528,58 +469,64 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
}
|
||||
}
|
||||
|
||||
#onPointerLockError = (e: Event) => {
|
||||
private onPointerLockError = (e: Event) => {
|
||||
console.log(e);
|
||||
this.stop();
|
||||
}
|
||||
|
||||
#onPointerLockRequested = () => {
|
||||
private onPointerLockRequested = () => {
|
||||
this.start();
|
||||
}
|
||||
|
||||
#onPointerLockExited = () => {
|
||||
this.#mouseDataProvider?.stop();
|
||||
private onPointerLockExited = () => {
|
||||
this.mouseDataProvider?.stop();
|
||||
}
|
||||
|
||||
handleEvent(event: Event) {
|
||||
switch (event.type) {
|
||||
case BxEvent.POINTER_LOCK_REQUESTED:
|
||||
this.#onPointerLockRequested();
|
||||
this.onPointerLockRequested();
|
||||
break;
|
||||
case BxEvent.POINTER_LOCK_EXITED:
|
||||
this.#onPointerLockExited();
|
||||
this.onPointerLockExited();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
init = () => {
|
||||
init() {
|
||||
if (!STATES.browser.capabilities.mkb) {
|
||||
this.initialized = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
|
||||
this.refreshPresetData();
|
||||
this.#enabled = false;
|
||||
this.enabled = false;
|
||||
|
||||
if (AppInterface) {
|
||||
this.#mouseDataProvider = new WebSocketMouseDataProvider(this);
|
||||
this.mouseDataProvider = new WebSocketMouseDataProvider(this);
|
||||
} else {
|
||||
this.#mouseDataProvider = new PointerLockMouseDataProvider(this);
|
||||
this.mouseDataProvider = new PointerLockMouseDataProvider(this);
|
||||
}
|
||||
this.#mouseDataProvider.init();
|
||||
this.mouseDataProvider.init();
|
||||
|
||||
window.addEventListener('keydown', this.#onKeyboardEvent);
|
||||
window.addEventListener('keyup', this.#onKeyboardEvent);
|
||||
window.addEventListener('keydown', this.onKeyboardEvent);
|
||||
window.addEventListener('keyup', this.onKeyboardEvent);
|
||||
|
||||
window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged);
|
||||
window.addEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.#onDialogShown);
|
||||
window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged);
|
||||
window.addEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.onDialogShown);
|
||||
|
||||
if (AppInterface) {
|
||||
// Android app doesn't support PointerLock API so we need to use a different method
|
||||
window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this);
|
||||
window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this);
|
||||
} else {
|
||||
document.addEventListener('pointerlockchange', this.#onPointerLockChange);
|
||||
document.addEventListener('pointerlockerror', this.#onPointerLockError);
|
||||
document.addEventListener('pointerlockchange', this.onPointerLockChange);
|
||||
document.addEventListener('pointerlockerror', this.onPointerLockError);
|
||||
}
|
||||
|
||||
this.#initMessage();
|
||||
this.#$message?.classList.add('bx-gone');
|
||||
MkbPopup.getInstance().reset();
|
||||
|
||||
if (AppInterface) {
|
||||
Toast.show(t('press-key-to-toggle-mkb', {key: `<b>F8</b>`}), t('virtual-controller'), {html: true});
|
||||
@@ -589,51 +536,62 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
}
|
||||
}
|
||||
|
||||
destroy = () => {
|
||||
this.#isPolling = false;
|
||||
this.#enabled = false;
|
||||
destroy() {
|
||||
if (!this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.initialized = false;
|
||||
this.isPolling = false;
|
||||
this.enabled = false;
|
||||
this.stop();
|
||||
|
||||
this.waitForMouseData(false);
|
||||
document.pointerLockElement && document.exitPointerLock();
|
||||
|
||||
window.removeEventListener('keydown', this.#onKeyboardEvent);
|
||||
window.removeEventListener('keyup', this.#onKeyboardEvent);
|
||||
window.removeEventListener('keydown', this.onKeyboardEvent);
|
||||
window.removeEventListener('keyup', this.onKeyboardEvent);
|
||||
|
||||
if (AppInterface) {
|
||||
window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this);
|
||||
window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this);
|
||||
} else {
|
||||
document.removeEventListener('pointerlockchange', this.#onPointerLockChange);
|
||||
document.removeEventListener('pointerlockerror', this.#onPointerLockError);
|
||||
document.removeEventListener('pointerlockchange', this.onPointerLockChange);
|
||||
document.removeEventListener('pointerlockerror', this.onPointerLockError);
|
||||
}
|
||||
|
||||
window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged);
|
||||
window.removeEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.#onDialogShown);
|
||||
window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged);
|
||||
window.removeEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.onDialogShown);
|
||||
|
||||
this.#mouseDataProvider?.destroy();
|
||||
this.mouseDataProvider?.destroy();
|
||||
|
||||
window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged);
|
||||
window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged);
|
||||
}
|
||||
|
||||
start = () => {
|
||||
if (!this.#enabled) {
|
||||
this.#enabled = true;
|
||||
Toast.show(t('virtual-controller'), t('enabled'), {instant: true});
|
||||
updateGamepadSlots() {
|
||||
// Set gamepad slot
|
||||
this.VIRTUAL_GAMEPAD.index = getPref<number>(PrefKey.MKB_P1_SLOT) - 1;
|
||||
}
|
||||
|
||||
start() {
|
||||
if (!this.enabled) {
|
||||
this.enabled = true;
|
||||
Toast.show(t('virtual-controller'), t('enabled'), { instant: true });
|
||||
}
|
||||
|
||||
this.#isPolling = true;
|
||||
this.#escKeyDownTime = -1;
|
||||
this.isPolling = true;
|
||||
this.escKeyDownTime = -1;
|
||||
|
||||
this.#resetGamepad();
|
||||
window.navigator.getGamepads = this.#patchedGetGamepads;
|
||||
this.resetGamepad();
|
||||
this.updateGamepadSlots();
|
||||
window.navigator.getGamepads = this.patchedGetGamepads;
|
||||
|
||||
this.waitForMouseData(false);
|
||||
|
||||
this.#mouseDataProvider?.start();
|
||||
this.mouseDataProvider?.start();
|
||||
|
||||
// Dispatch "gamepadconnected" event
|
||||
const virtualGamepad = this.#getVirtualGamepad();
|
||||
const virtualGamepad = this.getVirtualGamepad();
|
||||
virtualGamepad.connected = true;
|
||||
virtualGamepad.timestamp = performance.now();
|
||||
|
||||
@@ -643,46 +601,51 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
|
||||
window.BX_EXPOSED.stopTakRendering = true;
|
||||
|
||||
Toast.show(t('virtual-controller'), t('enabled'), {instant: true});
|
||||
Toast.show(t('virtual-controller'), t('enabled'), { instant: true });
|
||||
}
|
||||
|
||||
stop = () => {
|
||||
this.#enabled = false;
|
||||
this.#isPolling = false;
|
||||
this.#escKeyDownTime = -1;
|
||||
stop() {
|
||||
this.enabled = false;
|
||||
this.isPolling = false;
|
||||
this.escKeyDownTime = -1;
|
||||
|
||||
const virtualGamepad = this.#getVirtualGamepad();
|
||||
const virtualGamepad = this.getVirtualGamepad();
|
||||
if (virtualGamepad.connected) {
|
||||
// Dispatch "gamepaddisconnected" event
|
||||
this.#resetGamepad();
|
||||
this.resetGamepad();
|
||||
|
||||
virtualGamepad.connected = false;
|
||||
virtualGamepad.timestamp = performance.now();
|
||||
|
||||
BxEvent.dispatch(window, 'gamepaddisconnected', {
|
||||
gamepad: virtualGamepad,
|
||||
});
|
||||
gamepad: virtualGamepad,
|
||||
});
|
||||
|
||||
window.navigator.getGamepads = this.#nativeGetGamepads;
|
||||
window.navigator.getGamepads = this.nativeGetGamepads;
|
||||
}
|
||||
|
||||
this.waitForMouseData(true);
|
||||
this.#mouseDataProvider?.stop();
|
||||
this.mouseDataProvider?.stop();
|
||||
|
||||
// Toast.show(t('virtual-controller'), t('disabled'), {instant: true});
|
||||
}
|
||||
|
||||
static setupEvents() {
|
||||
isFullVersion() && window.addEventListener(BxEvent.STREAM_PLAYING, () => {
|
||||
if (STATES.currentStream.titleInfo?.details.hasMkbSupport) {
|
||||
// Enable native MKB in Android app
|
||||
if (AppInterface && getPref(PrefKey.NATIVE_MKB_ENABLED) === 'on') {
|
||||
AppInterface && NativeMkbHandler.getInstance().init();
|
||||
if (isFullVersion()) {
|
||||
window.addEventListener(BxEvent.STREAM_PLAYING, () => {
|
||||
if (STATES.currentStream.titleInfo?.details.hasMkbSupport) {
|
||||
// Enable native MKB in Android app
|
||||
NativeMkbHandler.getInstance()?.init();
|
||||
} else {
|
||||
EmulatedMkbHandler.getInstance()?.init();
|
||||
}
|
||||
} else if (getPref(PrefKey.MKB_ENABLED) && (AppInterface || !UserAgent.isMobile())) {
|
||||
BxLogger.info(EmulatedMkbHandler.LOG_TAG, 'Emulate MKB');
|
||||
EmulatedMkbHandler.getInstance().init();
|
||||
});
|
||||
|
||||
if (EmulatedMkbHandler.isAllowed()) {
|
||||
window.addEventListener(BxEvent.MKB_UPDATED, () => {
|
||||
EmulatedMkbHandler.getInstance()?.refreshPresetData();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
110
src/modules/mkb/mkb-popup.ts
Executable file
110
src/modules/mkb/mkb-popup.ts
Executable file
@@ -0,0 +1,110 @@
|
||||
import { CE, createButton, ButtonStyle, type BxButtonOptions } from "@/utils/html";
|
||||
import { t } from "@/utils/translation";
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
import { ShortcutAction } from "@/enums/shortcut-actions";
|
||||
import { SettingsDialog } from "../ui/dialog/settings-dialog";
|
||||
import type { MkbHandler } from "./base-mkb-handler";
|
||||
import { NativeMkbHandler } from "./native-mkb-handler";
|
||||
import { StreamSettings } from "@/utils/stream-settings";
|
||||
import { KeyHelper } from "./key-helper";
|
||||
|
||||
type MkbPopupType = 'virtual' | 'native';
|
||||
|
||||
export class MkbPopup {
|
||||
private static instance: MkbPopup;
|
||||
public static getInstance = () => MkbPopup.instance ?? (MkbPopup.instance = new MkbPopup());
|
||||
|
||||
private popupType!: MkbPopupType;
|
||||
private $popup!: HTMLElement;
|
||||
private $title!: HTMLElement;
|
||||
private $btnActivate!: HTMLButtonElement;
|
||||
|
||||
private mkbHandler!: MkbHandler;
|
||||
|
||||
constructor() {
|
||||
this.render();
|
||||
|
||||
window.addEventListener(BxEvent.KEYBOARD_SHORTCUTS_UPDATED, e => {
|
||||
const $newButton = this.createActivateButton();
|
||||
this.$btnActivate.replaceWith($newButton);
|
||||
this.$btnActivate = $newButton;
|
||||
});
|
||||
}
|
||||
|
||||
attachMkbHandler(handler: MkbHandler) {
|
||||
this.mkbHandler = handler;
|
||||
|
||||
// Set popupType
|
||||
this.popupType = (handler instanceof NativeMkbHandler) ? 'native' : 'virtual';
|
||||
this.$popup.dataset.type = this.popupType;
|
||||
|
||||
// Update popup title
|
||||
this.$title.innerText = t(this.popupType === 'native' ? 'native-mkb' : 'virtual-controller');
|
||||
}
|
||||
|
||||
toggleVisibility(show: boolean) {
|
||||
this.$popup.classList.toggle('bx-gone', !show);
|
||||
show && this.moveOffscreen(false);
|
||||
}
|
||||
|
||||
moveOffscreen(doMove: boolean) {
|
||||
this.$popup.classList.toggle('bx-offscreen', doMove);
|
||||
}
|
||||
|
||||
private createActivateButton() {
|
||||
const options: BxButtonOptions = {
|
||||
style: ButtonStyle.PRIMARY | ButtonStyle.TALL | ButtonStyle.FULL_WIDTH,
|
||||
label: t('activate'),
|
||||
onClick: this.onActivate,
|
||||
};
|
||||
|
||||
// Find shortcut key
|
||||
const shortcutKey = StreamSettings.findKeyboardShortcut(ShortcutAction.MKB_TOGGLE);
|
||||
if (shortcutKey) {
|
||||
options.secondaryText = t('press-key-to-toggle-mkb', { key: KeyHelper.codeToKeyName(shortcutKey) });
|
||||
}
|
||||
|
||||
return createButton(options);
|
||||
}
|
||||
|
||||
private onActivate = (e: Event) => {
|
||||
e.preventDefault();
|
||||
this.mkbHandler.toggle(true);
|
||||
}
|
||||
|
||||
private render() {
|
||||
this.$popup = CE('div', { class: 'bx-mkb-pointer-lock-msg bx-gone' },
|
||||
this.$title = CE('p'),
|
||||
this.$btnActivate = this.createActivateButton(),
|
||||
|
||||
CE('div', {},
|
||||
createButton({
|
||||
label: t('ignore'),
|
||||
style: ButtonStyle.GHOST,
|
||||
onClick: e => {
|
||||
e.preventDefault();
|
||||
this.mkbHandler.toggle(false);
|
||||
this.mkbHandler.waitForMouseData(false);
|
||||
},
|
||||
}),
|
||||
|
||||
createButton({
|
||||
label: t('manage'),
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
onClick: () => {
|
||||
const dialog = SettingsDialog.getInstance();
|
||||
dialog.focusTab('mkb');
|
||||
dialog.show();
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
document.documentElement.appendChild(this.$popup);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.toggleVisibility(true);
|
||||
this.moveOffscreen(false);
|
||||
}
|
||||
}
|
@@ -1,135 +0,0 @@
|
||||
import { t } from "@utils/translation";
|
||||
import { GamepadKey, MouseButtonCode, MouseMapTo, MkbPresetKey } from "@enums/mkb";
|
||||
import { EmulatedMkbHandler } from "./mkb-handler";
|
||||
import type { MkbPresetData, MkbConvertedPresetData } from "@/types/mkb";
|
||||
import type { PreferenceSettings } from "@/types/preferences";
|
||||
import { SettingElementType } from "@/utils/setting-element";
|
||||
|
||||
|
||||
export class MkbPreset {
|
||||
static MOUSE_SETTINGS: PreferenceSettings = {
|
||||
[MkbPresetKey.MOUSE_MAP_TO]: {
|
||||
label: t('map-mouse-to'),
|
||||
type: SettingElementType.OPTIONS,
|
||||
default: MouseMapTo[MouseMapTo.RS],
|
||||
options: {
|
||||
[MouseMapTo[MouseMapTo.RS]]: t('right-stick'),
|
||||
[MouseMapTo[MouseMapTo.LS]]: t('left-stick'),
|
||||
[MouseMapTo[MouseMapTo.OFF]]: t('off'),
|
||||
},
|
||||
},
|
||||
|
||||
[MkbPresetKey.MOUSE_SENSITIVITY_Y]: {
|
||||
label: t('horizontal-sensitivity'),
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 50,
|
||||
min: 1,
|
||||
max: 300,
|
||||
|
||||
params: {
|
||||
suffix: '%',
|
||||
exactTicks: 50,
|
||||
},
|
||||
},
|
||||
|
||||
[MkbPresetKey.MOUSE_SENSITIVITY_X]: {
|
||||
label: t('vertical-sensitivity'),
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 50,
|
||||
min: 1,
|
||||
max: 300,
|
||||
|
||||
params: {
|
||||
suffix: '%',
|
||||
exactTicks: 50,
|
||||
},
|
||||
},
|
||||
|
||||
[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]: {
|
||||
label: t('deadzone-counterweight'),
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 20,
|
||||
min: 1,
|
||||
max: 50,
|
||||
|
||||
params: {
|
||||
suffix: '%',
|
||||
exactTicks: 10,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static DEFAULT_PRESET: MkbPresetData = {
|
||||
'mapping': {
|
||||
// Use "e.code" value from https://keyjs.dev
|
||||
[GamepadKey.UP]: ['ArrowUp'],
|
||||
[GamepadKey.DOWN]: ['ArrowDown'],
|
||||
[GamepadKey.LEFT]: ['ArrowLeft'],
|
||||
[GamepadKey.RIGHT]: ['ArrowRight'],
|
||||
|
||||
[GamepadKey.LS_UP]: ['KeyW'],
|
||||
[GamepadKey.LS_DOWN]: ['KeyS'],
|
||||
[GamepadKey.LS_LEFT]: ['KeyA'],
|
||||
[GamepadKey.LS_RIGHT]: ['KeyD'],
|
||||
|
||||
[GamepadKey.RS_UP]: ['KeyI'],
|
||||
[GamepadKey.RS_DOWN]: ['KeyK'],
|
||||
[GamepadKey.RS_LEFT]: ['KeyJ'],
|
||||
[GamepadKey.RS_RIGHT]: ['KeyL'],
|
||||
|
||||
[GamepadKey.A]: ['Space', 'KeyE'],
|
||||
[GamepadKey.X]: ['KeyR'],
|
||||
[GamepadKey.B]: ['ControlLeft', 'Backspace'],
|
||||
[GamepadKey.Y]: ['KeyV'],
|
||||
|
||||
[GamepadKey.START]: ['Enter'],
|
||||
[GamepadKey.SELECT]: ['Tab'],
|
||||
|
||||
[GamepadKey.LB]: ['KeyC', 'KeyG'],
|
||||
[GamepadKey.RB]: ['KeyQ'],
|
||||
|
||||
[GamepadKey.HOME]: ['Backquote'],
|
||||
|
||||
[GamepadKey.RT]: [MouseButtonCode.LEFT_CLICK],
|
||||
[GamepadKey.LT]: [MouseButtonCode.RIGHT_CLICK],
|
||||
|
||||
[GamepadKey.L3]: ['ShiftLeft'],
|
||||
[GamepadKey.R3]: ['KeyF'],
|
||||
},
|
||||
|
||||
'mouse': {
|
||||
[MkbPresetKey.MOUSE_MAP_TO]: MouseMapTo[MouseMapTo.RS],
|
||||
[MkbPresetKey.MOUSE_SENSITIVITY_X]: 100,
|
||||
[MkbPresetKey.MOUSE_SENSITIVITY_Y]: 100,
|
||||
[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]: 20,
|
||||
},
|
||||
};
|
||||
|
||||
static convert(preset: MkbPresetData): MkbConvertedPresetData {
|
||||
const obj: MkbConvertedPresetData = {
|
||||
mapping: {},
|
||||
mouse: Object.assign({}, preset.mouse),
|
||||
};
|
||||
|
||||
for (const buttonIndex in preset.mapping) {
|
||||
for (const keyName of preset.mapping[parseInt(buttonIndex)]) {
|
||||
obj.mapping[keyName!] = parseInt(buttonIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-calculate mouse's sensitivities
|
||||
const mouse = obj.mouse;
|
||||
mouse[MkbPresetKey.MOUSE_SENSITIVITY_X] *= EmulatedMkbHandler.DEFAULT_PANNING_SENSITIVITY;
|
||||
mouse[MkbPresetKey.MOUSE_SENSITIVITY_Y] *= EmulatedMkbHandler.DEFAULT_PANNING_SENSITIVITY;
|
||||
mouse[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT] *= EmulatedMkbHandler.DEFAULT_DEADZONE_COUNTERWEIGHT;
|
||||
|
||||
const mouseMapTo = MouseMapTo[mouse[MkbPresetKey.MOUSE_MAP_TO]!];
|
||||
if (typeof mouseMapTo !== 'undefined') {
|
||||
mouse[MkbPresetKey.MOUSE_MAP_TO] = mouseMapTo;
|
||||
} else {
|
||||
mouse[MkbPresetKey.MOUSE_MAP_TO] = MkbPreset.MOUSE_SETTINGS[MkbPresetKey.MOUSE_MAP_TO].default;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
@@ -1,541 +0,0 @@
|
||||
import { CE, createButton, ButtonStyle, removeChildElements } from "@utils/html";
|
||||
import { t } from "@utils/translation";
|
||||
import { Dialog } from "@modules/dialog";
|
||||
import { KeyHelper } from "./key-helper";
|
||||
import { MkbPreset } from "./mkb-preset";
|
||||
import { EmulatedMkbHandler } from "./mkb-handler";
|
||||
import { BxIcon } from "@utils/bx-icon";
|
||||
import type { MkbPresetData, MkbStoredPresets } from "@/types/mkb";
|
||||
import { MkbPresetKey, GamepadKey, GamepadKeyName } from "@enums/mkb";
|
||||
import { deepClone } from "@utils/global";
|
||||
import { SettingElement } from "@/utils/setting-element";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref, setPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { MkbPresetsDb } from "@/utils/local-db/mkb-presets-db";
|
||||
import { BxLogger } from "@/utils/bx-logger";
|
||||
|
||||
|
||||
type MkbRemapperStates = {
|
||||
currentPresetId: number;
|
||||
presets: MkbStoredPresets;
|
||||
|
||||
editingPresetData?: MkbPresetData | null;
|
||||
isEditing: boolean;
|
||||
};
|
||||
|
||||
export class MkbRemapper {
|
||||
private readonly BUTTON_ORDERS = [
|
||||
GamepadKey.UP,
|
||||
GamepadKey.DOWN,
|
||||
GamepadKey.LEFT,
|
||||
GamepadKey.RIGHT,
|
||||
|
||||
GamepadKey.A,
|
||||
GamepadKey.B,
|
||||
GamepadKey.X,
|
||||
GamepadKey.Y,
|
||||
|
||||
GamepadKey.LB,
|
||||
GamepadKey.RB,
|
||||
GamepadKey.LT,
|
||||
GamepadKey.RT,
|
||||
|
||||
GamepadKey.SELECT,
|
||||
GamepadKey.START,
|
||||
GamepadKey.HOME,
|
||||
|
||||
GamepadKey.L3,
|
||||
GamepadKey.LS_UP,
|
||||
GamepadKey.LS_DOWN,
|
||||
GamepadKey.LS_LEFT,
|
||||
GamepadKey.LS_RIGHT,
|
||||
|
||||
GamepadKey.R3,
|
||||
GamepadKey.RS_UP,
|
||||
GamepadKey.RS_DOWN,
|
||||
GamepadKey.RS_LEFT,
|
||||
GamepadKey.RS_RIGHT,
|
||||
];
|
||||
|
||||
private static instance: MkbRemapper;
|
||||
public static getInstance = () => MkbRemapper.instance ?? (MkbRemapper.instance = new MkbRemapper());
|
||||
private readonly LOG_TAG = 'MkbRemapper';
|
||||
|
||||
private states: MkbRemapperStates = {
|
||||
currentPresetId: 0,
|
||||
presets: {},
|
||||
editingPresetData: null,
|
||||
isEditing: false,
|
||||
};
|
||||
|
||||
private $wrapper!: HTMLElement;
|
||||
private $presetsSelect!: HTMLSelectElement;
|
||||
private $activateButton!: HTMLButtonElement;
|
||||
|
||||
private $currentBindingKey!: HTMLElement;
|
||||
|
||||
private allKeyElements: HTMLElement[] = [];
|
||||
private allMouseElements: {[key in MkbPresetKey]?: HTMLElement} = {};
|
||||
|
||||
bindingDialog: Dialog;
|
||||
|
||||
private constructor() {
|
||||
BxLogger.info(this.LOG_TAG, 'constructor()');
|
||||
this.states.currentPresetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
|
||||
|
||||
this.bindingDialog = new Dialog({
|
||||
className: 'bx-binding-dialog',
|
||||
content: CE('div', {},
|
||||
CE('p', {}, t('press-to-bind')),
|
||||
CE('i', {}, t('press-esc-to-cancel')),
|
||||
),
|
||||
hideCloseButton: true,
|
||||
});
|
||||
}
|
||||
|
||||
private clearEventListeners = () => {
|
||||
window.removeEventListener('keydown', this.onKeyDown);
|
||||
window.removeEventListener('mousedown', this.onMouseDown);
|
||||
window.removeEventListener('wheel', this.onWheel);
|
||||
};
|
||||
|
||||
private bindKey = ($elm: HTMLElement, key: any) => {
|
||||
const buttonIndex = parseInt($elm.dataset.buttonIndex!);
|
||||
const keySlot = parseInt($elm.dataset.keySlot!);
|
||||
|
||||
// Ignore if bind the save key to the same element
|
||||
if ($elm.dataset.keyCode! === key.code) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Unbind duplicated keys
|
||||
for (const $otherElm of this.allKeyElements) {
|
||||
if ($otherElm.dataset.keyCode === key.code) {
|
||||
this.unbindKey($otherElm);
|
||||
}
|
||||
}
|
||||
|
||||
this.states.editingPresetData!.mapping[buttonIndex][keySlot] = key.code;
|
||||
$elm.textContent = key.name;
|
||||
$elm.dataset.keyCode = key.code;
|
||||
}
|
||||
|
||||
private unbindKey = ($elm: HTMLElement) => {
|
||||
const buttonIndex = parseInt($elm.dataset.buttonIndex!);
|
||||
const keySlot = parseInt($elm.dataset.keySlot!);
|
||||
|
||||
// Remove key from preset
|
||||
this.states.editingPresetData!.mapping[buttonIndex][keySlot] = null;
|
||||
$elm.textContent = '';
|
||||
delete $elm.dataset.keyCode;
|
||||
}
|
||||
|
||||
private onWheel = (e: WheelEvent) => {
|
||||
e.preventDefault();
|
||||
this.clearEventListeners();
|
||||
|
||||
this.bindKey(this.$currentBindingKey!, KeyHelper.getKeyFromEvent(e));
|
||||
window.setTimeout(() => this.bindingDialog.hide(), 200);
|
||||
};
|
||||
|
||||
private onMouseDown = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
this.clearEventListeners();
|
||||
|
||||
this.bindKey(this.$currentBindingKey!, KeyHelper.getKeyFromEvent(e));
|
||||
window.setTimeout(() => this.bindingDialog.hide(), 200);
|
||||
};
|
||||
|
||||
private onKeyDown = (e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.clearEventListeners();
|
||||
|
||||
if (e.code !== 'Escape') {
|
||||
this.bindKey(this.$currentBindingKey!, KeyHelper.getKeyFromEvent(e));
|
||||
}
|
||||
|
||||
window.setTimeout(() => this.bindingDialog.hide(), 200);
|
||||
};
|
||||
|
||||
private onBindingKey = (e: MouseEvent) => {
|
||||
if (!this.states.isEditing || e.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(e);
|
||||
|
||||
this.$currentBindingKey = e.target as HTMLElement;
|
||||
|
||||
window.addEventListener('keydown', this.onKeyDown);
|
||||
window.addEventListener('mousedown', this.onMouseDown);
|
||||
window.addEventListener('wheel', this.onWheel);
|
||||
|
||||
this.bindingDialog.show({title: this.$currentBindingKey.dataset.prompt!});
|
||||
};
|
||||
|
||||
private onContextMenu = (e: Event) => {
|
||||
e.preventDefault();
|
||||
if (!this.states.isEditing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.unbindKey(e.target as HTMLElement);
|
||||
};
|
||||
|
||||
private getPreset = (presetId: number) => {
|
||||
return this.states.presets[presetId];
|
||||
}
|
||||
|
||||
private getCurrentPreset = () => {
|
||||
let preset = this.getPreset(this.states.currentPresetId);
|
||||
if (!preset) {
|
||||
// Get the first preset in the list
|
||||
const firstPresetId = parseInt(Object.keys(this.states.presets)[0]);
|
||||
preset = this.states.presets[firstPresetId];
|
||||
this.states.currentPresetId = firstPresetId;
|
||||
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, firstPresetId);
|
||||
}
|
||||
|
||||
return preset;
|
||||
}
|
||||
|
||||
private switchPreset = (presetId: number) => {
|
||||
this.states.currentPresetId = presetId;
|
||||
const presetData = this.getCurrentPreset().data;
|
||||
|
||||
for (const $elm of this.allKeyElements) {
|
||||
const buttonIndex = parseInt($elm.dataset.buttonIndex!);
|
||||
const keySlot = parseInt($elm.dataset.keySlot!);
|
||||
|
||||
const buttonKeys = presetData.mapping[buttonIndex];
|
||||
if (buttonKeys && buttonKeys[keySlot]) {
|
||||
$elm.textContent = KeyHelper.codeToKeyName(buttonKeys[keySlot]!);
|
||||
$elm.dataset.keyCode = buttonKeys[keySlot]!;
|
||||
} else {
|
||||
$elm.textContent = '';
|
||||
delete $elm.dataset.keyCode;
|
||||
}
|
||||
}
|
||||
|
||||
let key: MkbPresetKey;
|
||||
for (key in this.allMouseElements) {
|
||||
const $elm = this.allMouseElements[key]!;
|
||||
let value = presetData.mouse[key];
|
||||
if (typeof value === 'undefined') {
|
||||
value = MkbPreset.MOUSE_SETTINGS[key].default;
|
||||
}
|
||||
|
||||
'setValue' in $elm && ($elm as any).setValue(value);
|
||||
}
|
||||
|
||||
// Update state of Activate button
|
||||
const activated = getPref(PrefKey.MKB_DEFAULT_PRESET_ID) === this.states.currentPresetId;
|
||||
this.$activateButton.disabled = activated;
|
||||
this.$activateButton.querySelector('span')!.textContent = activated ? t('activated') : t('activate');
|
||||
}
|
||||
|
||||
private async refresh() {
|
||||
// Clear presets select
|
||||
removeChildElements(this.$presetsSelect);
|
||||
|
||||
const presets = await MkbPresetsDb.getInstance().getPresets();
|
||||
|
||||
this.states.presets = presets;
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
let defaultPresetId;
|
||||
if (this.states.currentPresetId === 0) {
|
||||
this.states.currentPresetId = parseInt(Object.keys(presets)[0]);
|
||||
|
||||
defaultPresetId = this.states.currentPresetId;
|
||||
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, defaultPresetId);
|
||||
EmulatedMkbHandler.getInstance().refreshPresetData();
|
||||
} else {
|
||||
defaultPresetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
|
||||
}
|
||||
|
||||
for (let id in presets) {
|
||||
const preset = presets[id];
|
||||
let name = preset.name;
|
||||
if (id === defaultPresetId) {
|
||||
name = `🎮 ` + name;
|
||||
}
|
||||
|
||||
const $options = CE<HTMLOptionElement>('option', {value: id}, name);
|
||||
$options.selected = parseInt(id) === this.states.currentPresetId;
|
||||
|
||||
fragment.appendChild($options);
|
||||
};
|
||||
|
||||
this.$presetsSelect.appendChild(fragment);
|
||||
|
||||
// Update state of Activate button
|
||||
const activated = defaultPresetId === this.states.currentPresetId;
|
||||
this.$activateButton.disabled = activated;
|
||||
this.$activateButton.querySelector('span')!.textContent = activated ? t('activated') : t('activate');
|
||||
|
||||
!this.states.isEditing && this.switchPreset(this.states.currentPresetId);
|
||||
}
|
||||
|
||||
private toggleEditing = (force?: boolean) => {
|
||||
this.states.isEditing = typeof force !== 'undefined' ? force : !this.states.isEditing;
|
||||
this.$wrapper.classList.toggle('bx-editing', this.states.isEditing);
|
||||
|
||||
if (this.states.isEditing) {
|
||||
this.states.editingPresetData = deepClone(this.getCurrentPreset().data);
|
||||
} else {
|
||||
this.states.editingPresetData = null;
|
||||
}
|
||||
|
||||
|
||||
const childElements = this.$wrapper.querySelectorAll('select, button, input');
|
||||
for (const $elm of Array.from(childElements)) {
|
||||
if ($elm.parentElement!.parentElement!.classList.contains('bx-mkb-action-buttons')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let disable = !this.states.isEditing;
|
||||
|
||||
if ($elm.parentElement!.classList.contains('bx-mkb-preset-tools')) {
|
||||
disable = !disable;
|
||||
}
|
||||
|
||||
($elm as HTMLButtonElement).disabled = disable;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
this.$wrapper = CE('div', {class: 'bx-mkb-settings'});
|
||||
|
||||
this.$presetsSelect = CE<HTMLSelectElement>('select', {tabindex: -1});
|
||||
this.$presetsSelect.addEventListener('change', e => {
|
||||
this.switchPreset(parseInt((e.target as HTMLSelectElement).value));
|
||||
});
|
||||
|
||||
const promptNewName = (value: string) => {
|
||||
let newName: string | null = '';
|
||||
while (!newName) {
|
||||
newName = prompt(t('prompt-preset-name'), value);
|
||||
if (newName === null) {
|
||||
return false;
|
||||
}
|
||||
newName = newName.trim();
|
||||
}
|
||||
|
||||
return newName ? newName : false;
|
||||
};
|
||||
|
||||
const $header = CE('div', {class: 'bx-mkb-preset-tools'},
|
||||
this.$presetsSelect,
|
||||
// Rename button
|
||||
createButton({
|
||||
title: t('rename'),
|
||||
icon: BxIcon.CURSOR_TEXT,
|
||||
tabIndex: -1,
|
||||
onClick: async () => {
|
||||
const preset = this.getCurrentPreset();
|
||||
|
||||
let newName = promptNewName(preset.name);
|
||||
if (!newName || newName === preset.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update preset with new name
|
||||
preset.name = newName;
|
||||
|
||||
await MkbPresetsDb.getInstance().updatePreset(preset);
|
||||
await this.refresh();
|
||||
},
|
||||
}),
|
||||
|
||||
// New button
|
||||
createButton({
|
||||
icon: BxIcon.NEW,
|
||||
title: t('new'),
|
||||
tabIndex: -1,
|
||||
onClick: e => {
|
||||
let newName = promptNewName('');
|
||||
if (!newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new preset selected name
|
||||
MkbPresetsDb.getInstance().newPreset(newName, MkbPreset.DEFAULT_PRESET).then(id => {
|
||||
this.states.currentPresetId = id;
|
||||
this.refresh();
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
// Copy button
|
||||
createButton({
|
||||
icon: BxIcon.COPY,
|
||||
title: t('copy'),
|
||||
tabIndex: -1,
|
||||
onClick: e => {
|
||||
const preset = this.getCurrentPreset();
|
||||
|
||||
let newName = promptNewName(`${preset.name} (2)`);
|
||||
if (!newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new preset selected name
|
||||
MkbPresetsDb.getInstance().newPreset(newName, preset.data).then(id => {
|
||||
this.states.currentPresetId = id;
|
||||
this.refresh();
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
// Delete button
|
||||
createButton({
|
||||
icon: BxIcon.TRASH,
|
||||
style: ButtonStyle.DANGER,
|
||||
title: t('delete'),
|
||||
tabIndex: -1,
|
||||
onClick: e => {
|
||||
if (!confirm(t('confirm-delete-preset'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
MkbPresetsDb.getInstance().deletePreset(this.states.currentPresetId).then(id => {
|
||||
this.states.currentPresetId = 0;
|
||||
this.refresh();
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
this.$wrapper.appendChild($header);
|
||||
|
||||
const $rows = CE('div', {class: 'bx-mkb-settings-rows'},
|
||||
CE('i', {class: 'bx-mkb-note'}, t('right-click-to-unbind')),
|
||||
);
|
||||
|
||||
// Render keys
|
||||
const keysPerButton = 2;
|
||||
for (const buttonIndex of this.BUTTON_ORDERS) {
|
||||
const [buttonName, buttonPrompt] = GamepadKeyName[buttonIndex];
|
||||
|
||||
let $elm;
|
||||
const $fragment = document.createDocumentFragment();
|
||||
for (let i = 0; i < keysPerButton; i++) {
|
||||
$elm = CE('button', {
|
||||
type: 'button',
|
||||
'data-prompt': buttonPrompt,
|
||||
'data-button-index': buttonIndex,
|
||||
'data-key-slot': i,
|
||||
}, ' ');
|
||||
|
||||
$elm.addEventListener('mouseup', this.onBindingKey);
|
||||
$elm.addEventListener('contextmenu', this.onContextMenu);
|
||||
|
||||
$fragment.appendChild($elm);
|
||||
this.allKeyElements.push($elm);
|
||||
}
|
||||
|
||||
const $keyRow = CE('div', {class: 'bx-mkb-key-row'},
|
||||
CE('label', {title: buttonName}, buttonPrompt),
|
||||
$fragment,
|
||||
);
|
||||
|
||||
$rows.appendChild($keyRow);
|
||||
}
|
||||
|
||||
$rows.appendChild(CE('i', {class: 'bx-mkb-note'}, t('mkb-adjust-ingame-settings')),);
|
||||
|
||||
// Render mouse settings
|
||||
const $mouseSettings = document.createDocumentFragment();
|
||||
|
||||
for (const key in MkbPreset.MOUSE_SETTINGS) {
|
||||
const setting = MkbPreset.MOUSE_SETTINGS[key];
|
||||
const value = setting.default;
|
||||
|
||||
let $elm;
|
||||
const onChange = (e: Event, value: any) => {
|
||||
(this.states.editingPresetData!.mouse as any)[key] = value;
|
||||
};
|
||||
const $row = CE('label', {
|
||||
class: 'bx-settings-row',
|
||||
for: `bx_setting_${key}`
|
||||
},
|
||||
CE('span', {class: 'bx-settings-label'}, setting.label),
|
||||
$elm = SettingElement.render(setting.type, key, setting, value, onChange, setting.params),
|
||||
);
|
||||
|
||||
$mouseSettings.appendChild($row);
|
||||
this.allMouseElements[key as MkbPresetKey] = $elm;
|
||||
}
|
||||
|
||||
$rows.appendChild($mouseSettings);
|
||||
this.$wrapper.appendChild($rows);
|
||||
|
||||
// Render action buttons
|
||||
const $actionButtons = CE('div', {class: 'bx-mkb-action-buttons'},
|
||||
CE('div', {},
|
||||
// Edit button
|
||||
createButton({
|
||||
label: t('edit'),
|
||||
tabIndex: -1,
|
||||
onClick: e => this.toggleEditing(true),
|
||||
}),
|
||||
|
||||
// Activate button
|
||||
this.$activateButton = createButton({
|
||||
label: t('activate'),
|
||||
style: ButtonStyle.PRIMARY,
|
||||
tabIndex: -1,
|
||||
onClick: e => {
|
||||
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, this.states.currentPresetId);
|
||||
EmulatedMkbHandler.getInstance().refreshPresetData();
|
||||
|
||||
this.refresh();
|
||||
},
|
||||
}),
|
||||
),
|
||||
|
||||
CE('div', {},
|
||||
// Cancel button
|
||||
createButton({
|
||||
label: t('cancel'),
|
||||
style: ButtonStyle.GHOST,
|
||||
tabIndex: -1,
|
||||
onClick: e => {
|
||||
// Restore preset
|
||||
this.switchPreset(this.states.currentPresetId);
|
||||
this.toggleEditing(false);
|
||||
},
|
||||
}),
|
||||
|
||||
// Save button
|
||||
createButton({
|
||||
label: t('save'),
|
||||
style: ButtonStyle.PRIMARY,
|
||||
tabIndex: -1,
|
||||
onClick: e => {
|
||||
const updatedPreset = deepClone(this.getCurrentPreset());
|
||||
updatedPreset.data = this.states.editingPresetData as MkbPresetData;
|
||||
|
||||
MkbPresetsDb.getInstance().updatePreset(updatedPreset).then(id => {
|
||||
// If this is the default preset => refresh preset data
|
||||
if (id === getPref(PrefKey.MKB_DEFAULT_PRESET_ID)) {
|
||||
EmulatedMkbHandler.getInstance().refreshPresetData();
|
||||
}
|
||||
|
||||
this.toggleEditing(false);
|
||||
this.refresh();
|
||||
});
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
this.$wrapper.appendChild($actionButtons);
|
||||
|
||||
this.toggleEditing(false);
|
||||
this.refresh();
|
||||
return this.$wrapper;
|
||||
}
|
||||
}
|
54
src/modules/mkb/mouse-cursor-hider.ts
Normal file → Executable file
54
src/modules/mkb/mouse-cursor-hider.ts
Normal file → Executable file
@@ -1,34 +1,52 @@
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
|
||||
export class MouseCursorHider {
|
||||
static #timeout: number | null;
|
||||
static #cursorVisible = true;
|
||||
private static instance: MouseCursorHider | null | undefined;
|
||||
public static getInstance(): typeof MouseCursorHider['instance'] {
|
||||
if (typeof MouseCursorHider.instance === 'undefined') {
|
||||
if (!getPref(PrefKey.MKB_ENABLED) && getPref(PrefKey.MKB_HIDE_IDLE_CURSOR)) {
|
||||
MouseCursorHider.instance = new MouseCursorHider();
|
||||
} else {
|
||||
MouseCursorHider.instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
static show() {
|
||||
return MouseCursorHider.instance;
|
||||
}
|
||||
|
||||
private timeoutId!: number | null;
|
||||
private isCursorVisible = true;
|
||||
|
||||
show() {
|
||||
document.body && (document.body.style.cursor = 'unset');
|
||||
MouseCursorHider.#cursorVisible = true;
|
||||
this.isCursorVisible = true;
|
||||
}
|
||||
|
||||
static hide() {
|
||||
hide() {
|
||||
document.body && (document.body.style.cursor = 'none');
|
||||
MouseCursorHider.#timeout = null;
|
||||
MouseCursorHider.#cursorVisible = false;
|
||||
this.timeoutId = null;
|
||||
this.isCursorVisible = false;
|
||||
}
|
||||
|
||||
static onMouseMove(e: MouseEvent) {
|
||||
onMouseMove = (e: MouseEvent) => {
|
||||
// Toggle cursor
|
||||
!MouseCursorHider.#cursorVisible && MouseCursorHider.show();
|
||||
!this.isCursorVisible && this.show();
|
||||
// Setup timeout
|
||||
MouseCursorHider.#timeout && clearTimeout(MouseCursorHider.#timeout);
|
||||
MouseCursorHider.#timeout = window.setTimeout(MouseCursorHider.hide, 3000);
|
||||
this.timeoutId && clearTimeout(this.timeoutId);
|
||||
this.timeoutId = window.setTimeout(this.hide, 3000);
|
||||
}
|
||||
|
||||
static start() {
|
||||
MouseCursorHider.show();
|
||||
document.addEventListener('mousemove', MouseCursorHider.onMouseMove);
|
||||
start() {
|
||||
this.show();
|
||||
document.addEventListener('mousemove', this.onMouseMove);
|
||||
}
|
||||
|
||||
static stop() {
|
||||
MouseCursorHider.#timeout && clearTimeout(MouseCursorHider.#timeout);
|
||||
document.removeEventListener('mousemove', MouseCursorHider.onMouseMove);
|
||||
MouseCursorHider.show();
|
||||
stop() {
|
||||
this.timeoutId && clearTimeout(this.timeoutId);
|
||||
this.timeoutId = null;
|
||||
|
||||
document.removeEventListener('mousemove', this.onMouseMove);
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
|
232
src/modules/mkb/native-mkb-handler.ts
Normal file → Executable file
232
src/modules/mkb/native-mkb-handler.ts
Normal file → Executable file
@@ -4,10 +4,14 @@ import { AppInterface, STATES } from "@/utils/global";
|
||||
import { MkbHandler } from "./base-mkb-handler";
|
||||
import { t } from "@/utils/translation";
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
import { ButtonStyle, CE, createButton } from "@/utils/html";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { BxLogger } from "@/utils/bx-logger";
|
||||
import { MkbPopup } from "./mkb-popup";
|
||||
import { KeyHelper } from "./key-helper";
|
||||
import { StreamSettings } from "@/utils/stream-settings";
|
||||
import { ShortcutAction } from "@/enums/shortcut-actions";
|
||||
import { NativeMkbMode } from "@/enums/pref-values";
|
||||
|
||||
type NativeMouseData = {
|
||||
X: number,
|
||||
@@ -15,7 +19,7 @@ type NativeMouseData = {
|
||||
Buttons: number,
|
||||
WheelX: number,
|
||||
WheelY: number,
|
||||
Type? : 0, // 0: Relative, 1: Absolute
|
||||
Type?: 0, // 0: Relative, 1: Absolute
|
||||
}
|
||||
|
||||
type XcloudInputSink = {
|
||||
@@ -23,30 +27,47 @@ type XcloudInputSink = {
|
||||
}
|
||||
|
||||
export class NativeMkbHandler extends MkbHandler {
|
||||
private static instance: NativeMkbHandler;
|
||||
public static getInstance = () => NativeMkbHandler.instance ?? (NativeMkbHandler.instance = new NativeMkbHandler());
|
||||
private static instance: NativeMkbHandler | null | undefined;
|
||||
public static getInstance(): typeof NativeMkbHandler['instance'] {
|
||||
if (typeof NativeMkbHandler.instance === 'undefined') {
|
||||
if (NativeMkbHandler.isAllowed()) {
|
||||
NativeMkbHandler.instance = new NativeMkbHandler();
|
||||
} else {
|
||||
NativeMkbHandler.instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
return NativeMkbHandler.instance;
|
||||
}
|
||||
private readonly LOG_TAG = 'NativeMkbHandler';
|
||||
|
||||
#pointerClient: PointerClient | undefined;
|
||||
#enabled: boolean = false;
|
||||
static isAllowed = () => {
|
||||
return STATES.browser.capabilities.emulatedNativeMkb && getPref<NativeMkbMode>(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON;
|
||||
}
|
||||
|
||||
#mouseButtonsPressed = 0;
|
||||
#mouseWheelX = 0;
|
||||
#mouseWheelY = 0;
|
||||
private pointerClient: PointerClient | undefined;
|
||||
private enabled = false;
|
||||
|
||||
#mouseVerticalMultiply = 0;
|
||||
#mouseHorizontalMultiply = 0;
|
||||
private mouseButtonsPressed = 0;
|
||||
private mouseWheelX = 0;
|
||||
private mouseWheelY = 0;
|
||||
|
||||
#inputSink: XcloudInputSink | undefined;
|
||||
private mouseVerticalMultiply = 0;
|
||||
private mouseHorizontalMultiply = 0;
|
||||
|
||||
#$message?: HTMLElement;
|
||||
private inputSink: XcloudInputSink | undefined;
|
||||
|
||||
private popup!: MkbPopup;
|
||||
|
||||
private constructor() {
|
||||
super();
|
||||
BxLogger.info(this.LOG_TAG, 'constructor()');
|
||||
|
||||
this.popup = MkbPopup.getInstance();
|
||||
this.popup.attachMkbHandler(this);
|
||||
}
|
||||
|
||||
#onKeyboardEvent(e: KeyboardEvent) {
|
||||
private onKeyboardEvent(e: KeyboardEvent) {
|
||||
if (e.type === 'keyup' && e.code === 'F8') {
|
||||
e.preventDefault();
|
||||
this.toggle();
|
||||
@@ -54,110 +75,63 @@ export class NativeMkbHandler extends MkbHandler {
|
||||
}
|
||||
}
|
||||
|
||||
#onPointerLockRequested(e: Event) {
|
||||
private onPointerLockRequested(e: Event) {
|
||||
AppInterface.requestPointerCapture();
|
||||
this.start();
|
||||
}
|
||||
|
||||
#onPointerLockExited(e: Event) {
|
||||
private onPointerLockExited(e: Event) {
|
||||
AppInterface.releasePointerCapture();
|
||||
this.stop();
|
||||
}
|
||||
|
||||
#onPollingModeChanged = (e: Event) => {
|
||||
if (!this.#$message) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mode = (e as any).mode;
|
||||
if (mode === 'none') {
|
||||
this.#$message.classList.remove('bx-offscreen');
|
||||
} else {
|
||||
this.#$message.classList.add('bx-offscreen');
|
||||
}
|
||||
private onPollingModeChanged = (e: Event) => {
|
||||
const move = window.BX_STREAM_SETTINGS.xCloudPollingMode !== 'none';
|
||||
this.popup.moveOffscreen(move);
|
||||
}
|
||||
|
||||
#onDialogShown = () => {
|
||||
private onDialogShown = () => {
|
||||
document.pointerLockElement && document.exitPointerLock();
|
||||
}
|
||||
|
||||
#initMessage() {
|
||||
if (!this.#$message) {
|
||||
this.#$message = CE('div', {'class': 'bx-mkb-pointer-lock-msg'},
|
||||
CE('div', {},
|
||||
CE('p', {}, t('native-mkb')),
|
||||
CE('p', {}, t('press-key-to-toggle-mkb', {key: 'F8'})),
|
||||
),
|
||||
|
||||
CE('div', {'data-type': 'native'},
|
||||
createButton({
|
||||
style: ButtonStyle.PRIMARY | ButtonStyle.FULL_WIDTH | ButtonStyle.TALL,
|
||||
label: t('activate'),
|
||||
onClick: ((e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.toggle(true);
|
||||
}).bind(this),
|
||||
}),
|
||||
|
||||
createButton({
|
||||
style: ButtonStyle.GHOST | ButtonStyle.FULL_WIDTH,
|
||||
label: t('ignore'),
|
||||
onClick: e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.#$message?.classList.add('bx-gone');
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.#$message.isConnected) {
|
||||
document.documentElement.appendChild(this.#$message);
|
||||
}
|
||||
}
|
||||
|
||||
handleEvent(event: Event) {
|
||||
switch (event.type) {
|
||||
case 'keyup':
|
||||
this.#onKeyboardEvent(event as KeyboardEvent);
|
||||
this.onKeyboardEvent(event as KeyboardEvent);
|
||||
break;
|
||||
|
||||
case BxEvent.XCLOUD_DIALOG_SHOWN:
|
||||
this.#onDialogShown();
|
||||
this.onDialogShown();
|
||||
break;
|
||||
|
||||
case BxEvent.POINTER_LOCK_REQUESTED:
|
||||
this.#onPointerLockRequested(event);
|
||||
this.onPointerLockRequested(event);
|
||||
break;
|
||||
case BxEvent.POINTER_LOCK_EXITED:
|
||||
this.#onPointerLockExited(event);
|
||||
this.onPointerLockExited(event);
|
||||
break;
|
||||
|
||||
case BxEvent.XCLOUD_POLLING_MODE_CHANGED:
|
||||
this.#onPollingModeChanged(event);
|
||||
this.onPollingModeChanged(event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
this.#pointerClient = PointerClient.getInstance();
|
||||
this.#inputSink = window.BX_EXPOSED.inputSink;
|
||||
this.pointerClient = PointerClient.getInstance();
|
||||
this.inputSink = window.BX_EXPOSED.inputSink;
|
||||
|
||||
// Stop keyboard input at startup
|
||||
this.#updateInputConfigurationAsync(false);
|
||||
this.updateInputConfigurationAsync(false);
|
||||
|
||||
try {
|
||||
this.#pointerClient.start(STATES.pointerServerPort, this);
|
||||
this.pointerClient.start(STATES.pointerServerPort, this);
|
||||
} catch (e) {
|
||||
Toast.show('Cannot enable Mouse & Keyboard feature');
|
||||
}
|
||||
|
||||
this.#mouseVerticalMultiply = getPref(PrefKey.NATIVE_MKB_SCROLL_VERTICAL_SENSITIVITY);
|
||||
this.#mouseHorizontalMultiply = getPref(PrefKey.NATIVE_MKB_SCROLL_HORIZONTAL_SENSITIVITY);
|
||||
this.mouseVerticalMultiply = getPref(PrefKey.NATIVE_MKB_SCROLL_VERTICAL_SENSITIVITY);
|
||||
this.mouseHorizontalMultiply = getPref(PrefKey.NATIVE_MKB_SCROLL_HORIZONTAL_SENSITIVITY);
|
||||
|
||||
window.addEventListener('keyup', this);
|
||||
|
||||
@@ -166,14 +140,13 @@ export class NativeMkbHandler extends MkbHandler {
|
||||
window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this);
|
||||
window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this);
|
||||
|
||||
this.#initMessage();
|
||||
|
||||
if (AppInterface) {
|
||||
Toast.show(t('press-key-to-toggle-mkb', {key: `<b>F8</b>`}), t('native-mkb'), {html: true});
|
||||
this.#$message?.classList.add('bx-gone');
|
||||
} else {
|
||||
this.#$message?.classList.remove('bx-gone');
|
||||
const shortcutKey = StreamSettings.findKeyboardShortcut(ShortcutAction.MKB_TOGGLE);
|
||||
if (shortcutKey) {
|
||||
const msg = t('press-key-to-toggle-mkb', { key: `<b>${KeyHelper.codeToKeyName(shortcutKey)}</b>` });
|
||||
Toast.show(msg, t('native-mkb'), { html: true });
|
||||
}
|
||||
|
||||
this.waitForMouseData(false);
|
||||
}
|
||||
|
||||
toggle(force?: boolean) {
|
||||
@@ -181,7 +154,7 @@ export class NativeMkbHandler extends MkbHandler {
|
||||
if (typeof force !== 'undefined') {
|
||||
setEnable = force;
|
||||
} else {
|
||||
setEnable = !this.#enabled;
|
||||
setEnable = !this.enabled;
|
||||
}
|
||||
|
||||
if (setEnable) {
|
||||
@@ -191,7 +164,7 @@ export class NativeMkbHandler extends MkbHandler {
|
||||
}
|
||||
}
|
||||
|
||||
#updateInputConfigurationAsync(enabled: boolean) {
|
||||
private updateInputConfigurationAsync(enabled: boolean) {
|
||||
window.BX_EXPOSED.streamSession.updateInputConfigurationAsync({
|
||||
enableKeyboardInput: enabled,
|
||||
enableMouseInput: enabled,
|
||||
@@ -201,27 +174,27 @@ export class NativeMkbHandler extends MkbHandler {
|
||||
}
|
||||
|
||||
start() {
|
||||
this.#resetMouseInput();
|
||||
this.#enabled = true;
|
||||
this.resetMouseInput();
|
||||
this.enabled = true;
|
||||
|
||||
this.#updateInputConfigurationAsync(true);
|
||||
this.updateInputConfigurationAsync(true);
|
||||
|
||||
window.BX_EXPOSED.stopTakRendering = true;
|
||||
this.#$message?.classList.add('bx-gone');
|
||||
this.waitForMouseData(false);
|
||||
|
||||
Toast.show(t('native-mkb'), t('enabled'), {instant: true});
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.#resetMouseInput();
|
||||
this.#enabled = false;
|
||||
this.#updateInputConfigurationAsync(false);
|
||||
this.resetMouseInput();
|
||||
this.enabled = false;
|
||||
this.updateInputConfigurationAsync(false);
|
||||
|
||||
this.#$message?.classList.remove('bx-gone');
|
||||
this.waitForMouseData(true);
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.#pointerClient?.stop();
|
||||
this.pointerClient?.stop();
|
||||
window.removeEventListener('keyup', this);
|
||||
|
||||
window.removeEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this);
|
||||
@@ -229,16 +202,16 @@ export class NativeMkbHandler extends MkbHandler {
|
||||
window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this);
|
||||
window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this);
|
||||
|
||||
this.#$message?.classList.add('bx-gone');
|
||||
this.waitForMouseData(false);
|
||||
}
|
||||
|
||||
handleMouseMove(data: MkbMouseMove): void {
|
||||
this.#sendMouseInput({
|
||||
this.sendMouseInput({
|
||||
X: data.movementX,
|
||||
Y: data.movementY,
|
||||
Buttons: this.#mouseButtonsPressed,
|
||||
WheelX: this.#mouseWheelX,
|
||||
WheelY: this.#mouseWheelY,
|
||||
Buttons: this.mouseButtonsPressed,
|
||||
WheelX: this.mouseWheelX,
|
||||
WheelY: this.mouseWheelY,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -246,71 +219,72 @@ export class NativeMkbHandler extends MkbHandler {
|
||||
const { pointerButton, pressed } = data;
|
||||
|
||||
if (pressed) {
|
||||
this.#mouseButtonsPressed |= pointerButton!;
|
||||
this.mouseButtonsPressed |= pointerButton!;
|
||||
} else {
|
||||
this.#mouseButtonsPressed ^= pointerButton!;
|
||||
this.mouseButtonsPressed ^= pointerButton!;
|
||||
}
|
||||
this.#mouseButtonsPressed = Math.max(0, this.#mouseButtonsPressed);
|
||||
this.mouseButtonsPressed = Math.max(0, this.mouseButtonsPressed);
|
||||
|
||||
this.#sendMouseInput({
|
||||
this.sendMouseInput({
|
||||
X: 0,
|
||||
Y: 0,
|
||||
Buttons: this.#mouseButtonsPressed,
|
||||
WheelX: this.#mouseWheelX,
|
||||
WheelY: this.#mouseWheelY,
|
||||
Buttons: this.mouseButtonsPressed,
|
||||
WheelX: this.mouseWheelX,
|
||||
WheelY: this.mouseWheelY,
|
||||
});
|
||||
}
|
||||
|
||||
handleMouseWheel(data: MkbMouseWheel): boolean {
|
||||
const { vertical, horizontal } = data;
|
||||
|
||||
this.#mouseWheelX = horizontal;
|
||||
if (this.#mouseHorizontalMultiply && this.#mouseHorizontalMultiply !== 1) {
|
||||
this.#mouseWheelX *= this.#mouseHorizontalMultiply;
|
||||
this.mouseWheelX = horizontal;
|
||||
if (this.mouseHorizontalMultiply && this.mouseHorizontalMultiply !== 1) {
|
||||
this.mouseWheelX *= this.mouseHorizontalMultiply;
|
||||
}
|
||||
|
||||
this.#mouseWheelY = vertical;
|
||||
if (this.#mouseVerticalMultiply && this.#mouseVerticalMultiply !== 1) {
|
||||
this.#mouseWheelY *= this.#mouseVerticalMultiply;
|
||||
this.mouseWheelY = vertical;
|
||||
if (this.mouseVerticalMultiply && this.mouseVerticalMultiply !== 1) {
|
||||
this.mouseWheelY *= this.mouseVerticalMultiply;
|
||||
}
|
||||
|
||||
this.#sendMouseInput({
|
||||
this.sendMouseInput({
|
||||
X: 0,
|
||||
Y: 0,
|
||||
Buttons: this.#mouseButtonsPressed,
|
||||
WheelX: this.#mouseWheelX,
|
||||
WheelY: this.#mouseWheelY,
|
||||
Buttons: this.mouseButtonsPressed,
|
||||
WheelX: this.mouseWheelX,
|
||||
WheelY: this.mouseWheelY,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
setVerticalScrollMultiplier(vertical: number) {
|
||||
this.#mouseVerticalMultiply = vertical;
|
||||
this.mouseVerticalMultiply = vertical;
|
||||
}
|
||||
|
||||
setHorizontalScrollMultiplier(horizontal: number) {
|
||||
this.#mouseHorizontalMultiply = horizontal;
|
||||
this.mouseHorizontalMultiply = horizontal;
|
||||
}
|
||||
|
||||
waitForMouseData(enabled: boolean): void {
|
||||
waitForMouseData(showPopup: boolean) {
|
||||
this.popup.toggleVisibility(showPopup);
|
||||
}
|
||||
|
||||
isEnabled(): boolean {
|
||||
return this.#enabled;
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
#sendMouseInput(data: NativeMouseData) {
|
||||
private sendMouseInput(data: NativeMouseData) {
|
||||
data.Type = 0; // Relative
|
||||
this.#inputSink?.onMouseInput(data);
|
||||
this.inputSink?.onMouseInput(data);
|
||||
}
|
||||
|
||||
#resetMouseInput() {
|
||||
this.#mouseButtonsPressed = 0;
|
||||
this.#mouseWheelX = 0;
|
||||
this.#mouseWheelY = 0;
|
||||
private resetMouseInput() {
|
||||
this.mouseButtonsPressed = 0;
|
||||
this.mouseWheelX = 0;
|
||||
this.mouseWheelY = 0;
|
||||
|
||||
this.#sendMouseInput({
|
||||
this.sendMouseInput({
|
||||
X: 0,
|
||||
Y: 0,
|
||||
Buttons: 0,
|
||||
|
0
src/modules/mkb/pointer-client.ts
Normal file → Executable file
0
src/modules/mkb/pointer-client.ts
Normal file → Executable file
Reference in New Issue
Block a user