diff --git a/frontend/react/src/audio/audiolibrary.ts b/frontend/react/src/audio/audiolibrary.ts
index 67df36dd..64b9ad1e 100644
--- a/frontend/react/src/audio/audiolibrary.ts
+++ b/frontend/react/src/audio/audiolibrary.ts
@@ -11,13 +11,13 @@ if (!window.AudioBuffer.prototype.copyFromChannel) {
export class Effect {
name: string;
- context: AudioContext;
+ context: AudioContext | OfflineAudioContext;
input: GainNode;
effect: GainNode | BiquadFilterNode | null;
bypassed: boolean;
output: GainNode;
- constructor(context: AudioContext) {
+ constructor(context: AudioContext | OfflineAudioContext) {
this.name = "effect";
this.context = context;
this.input = this.context.createGain();
@@ -45,14 +45,14 @@ export class Effect {
}
export class Sample {
- context: AudioContext;
+ context: AudioContext | OfflineAudioContext;
buffer: AudioBufferSourceNode;
sampleBuffer: AudioBuffer | null;
rawBuffer: ArrayBuffer | null;
loaded: boolean;
output: GainNode;
- constructor(context: AudioContext) {
+ constructor(context: AudioContext | OfflineAudioContext) {
this.context = context;
this.buffer = this.context.createBufferSource();
this.buffer.start();
@@ -94,7 +94,7 @@ export class Sample {
}
export class AmpEnvelope {
- context: AudioContext;
+ context: AudioContext | OfflineAudioContext;
output: GainNode;
partials: any[];
velocity: number;
@@ -104,7 +104,7 @@ export class AmpEnvelope {
#sustain: number;
#release: number;
- constructor(context: AudioContext, gain: number = 1) {
+ constructor(context: AudioContext | OfflineAudioContext, gain: number = 1) {
this.context = context;
this.output = this.context.createGain();
this.output.gain.value = gain;
@@ -179,7 +179,7 @@ export class AmpEnvelope {
}
export class Voice {
- context: AudioContext;
+ context: AudioContext | OfflineAudioContext;
type: string;
value: number;
gain: number;
@@ -187,7 +187,7 @@ export class Voice {
partials: any[];
ampEnvelope: AmpEnvelope;
- constructor(context: AudioContext, gain: number = 0.1, type: string = "sawtooth") {
+ constructor(context: AudioContext | OfflineAudioContext, gain: number = 0.1, type: string = "sawtooth") {
this.context = context;
this.type = type;
this.value = -1;
@@ -266,7 +266,7 @@ export class Voice {
export class Noise extends Voice {
#length: number;
- constructor(context: AudioContext, gain: number) {
+ constructor(context: AudioContext | OfflineAudioContext, gain: number) {
super(context, gain);
this.#length = 2;
}
@@ -307,7 +307,7 @@ export class Noise extends Voice {
}
export class Filter extends Effect {
- constructor(context: AudioContext, type: string = "lowpass", cutoff: number = 1000, resonance: number = 0.9) {
+ constructor(context: AudioContext | OfflineAudioContext, type: string = "lowpass", cutoff: number = 1000, resonance: number = 0.9) {
super(context);
this.name = "filter";
if (this.effect instanceof BiquadFilterNode) {
diff --git a/frontend/react/src/audio/audiomanager.ts b/frontend/react/src/audio/audiomanager.ts
index 505adbd1..0450876f 100644
--- a/frontend/react/src/audio/audiomanager.ts
+++ b/frontend/react/src/audio/audiomanager.ts
@@ -38,12 +38,24 @@ export class AudioManager {
constructor() {
ConfigLoadedEvent.on((config: OlympusConfig) => {
- config.WSPort ? this.setPort(config.WSPort) : this.setEndpoint(config.WSEndpoint);
+ config.audio.WSPort ? this.setPort(config.audio.WSPort) : this.setEndpoint(config.audio.WSEndpoint);
});
setInterval(() => {
this.#syncRadioSettings();
}, 1000);
+
+ let PTTKeys = ["KeyZ", "KeyX", "KeyC", "KeyV", "KeyB", "KeyN", "KeyM", "KeyK", "KeyL"];
+ PTTKeys.forEach((key, idx) => {
+ getApp()
+ .getShortcutManager()
+ .addShortcut(`PTT${idx}Active`, {
+ label: `PTT ${idx} active`,
+ keyDownCallback: () => this.getSinks()[idx]?.setPtt(true),
+ keyUpCallback: () => this.getSinks()[idx]?.setPtt(false),
+ code: key
+ });
+ });
}
start() {
diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts
index 5a740f82..f09399ca 100644
--- a/frontend/react/src/constants/constants.ts
+++ b/frontend/react/src/constants/constants.ts
@@ -276,7 +276,7 @@ export enum OlympusState {
OPTIONS = "Options",
AUDIO = "Audio",
AIRBASE = "Airbase",
- GAME_MASTER = "Game master",
+ GAME_MASTER = "Game master"
}
export const NO_SUBSTATE = "No substate";
@@ -310,7 +310,12 @@ export enum SpawnSubState {
SPAWN_EFFECT = "Effect",
}
-export type OlympusSubState = DrawSubState | JTACSubState | SpawnSubState | string;
+export enum OptionsSubstate {
+ NO_SUBSTATE = "No substate",
+ KEYBIND = "Keybind"
+}
+
+export type OlympusSubState = DrawSubState | JTACSubState | SpawnSubState | OptionsSubstate | string;
export const IADSTypes = ["AAA", "SAM Site", "Radar (EWR)"];
export const IADSDensities: { [key: string]: number } = {
diff --git a/frontend/react/src/events.ts b/frontend/react/src/events.ts
index 722b980c..379f34fa 100644
--- a/frontend/react/src/events.ts
+++ b/frontend/react/src/events.ts
@@ -5,6 +5,7 @@ import { CommandModeOptions, OlympusConfig, ServerStatus } from "./interfaces";
import { CoalitionCircle } from "./map/coalitionarea/coalitioncircle";
import { CoalitionPolygon } from "./map/coalitionarea/coalitionpolygon";
import { Airbase } from "./mission/airbase";
+import { Shortcut } from "./shortcut/shortcut";
import { MapHiddenTypes, MapOptions } from "./types/types";
import { ContextAction } from "./unit/contextaction";
import { ContextActionSet } from "./unit/contextactionset";
@@ -108,6 +109,45 @@ export class HideMenuEvent {
}
}
+export class ShortcutsChangedEvent {
+ static on(callback: (shortcuts: {[key: string]: Shortcut}) => void) {
+ document.addEventListener(this.name, (ev: CustomEventInit) => {
+ callback(ev.detail.shortcuts);
+ });
+ }
+
+ static dispatch(shortcuts: {[key: string]: Shortcut}) {
+ document.dispatchEvent(new CustomEvent(this.name, {detail: {shortcuts}}));
+ console.log(`Event ${this.name} dispatched`);
+ }
+}
+
+export class ShortcutChangedEvent {
+ static on(callback: (shortcut: Shortcut) => void) {
+ document.addEventListener(this.name, (ev: CustomEventInit) => {
+ callback(ev.detail.shortcut);
+ });
+ }
+
+ static dispatch(shortcut: Shortcut) {
+ document.dispatchEvent(new CustomEvent(this.name, {detail: {shortcut}}));
+ console.log(`Event ${this.name} dispatched`);
+ }
+}
+
+export class BindShortcutRequestEvent {
+ static on(callback: (shortcut: Shortcut) => void) {
+ document.addEventListener(this.name, (ev: CustomEventInit) => {
+ callback(ev.detail.shortcut);
+ });
+ }
+
+ static dispatch(shortcut: Shortcut) {
+ document.dispatchEvent(new CustomEvent(this.name, {detail: {shortcut}}));
+ console.log(`Event ${this.name} dispatched`);
+ }
+}
+
/************** Map events ***************/
export class HiddenTypesChangedEvent {
static on(callback: (hiddenTypes: MapHiddenTypes) => void) {
diff --git a/frontend/react/src/interfaces.ts b/frontend/react/src/interfaces.ts
index 14fd813d..ef3ee240 100644
--- a/frontend/react/src/interfaces.ts
+++ b/frontend/react/src/interfaces.ts
@@ -1,26 +1,37 @@
import { LatLng } from "leaflet";
+import { MapOptions } from "./types/types";
export interface OlympusConfig {
- port: number;
- elevationProvider: {
- provider: string;
- username: string | null;
- password: string | null;
- };
- mapLayers: {
- [key: string]: {
- urlTemplate: string;
- minZoom: number;
- maxZoom: number;
- attribution?: string;
+ frontend: {
+ port: number;
+ elevationProvider: {
+ provider: string;
+ username: string | null;
+ password: string | null;
+ };
+ mapLayers: {
+ [key: string]: {
+ urlTemplate: string;
+ minZoom: number;
+ maxZoom: number;
+ attribution?: string;
+ };
+ };
+ mapMirrors: {
+ [key: string]: string;
};
};
- mapMirrors: {
- [key: string]: string;
+ audio: {
+ SRSPort: number;
+ WSPort?: number;
+ WSEndpoint?: string;
};
- SRSPort: number;
- WSPort?: number;
- WSEndpoint?: string;
+ profiles?: ProfileOptions;
+}
+
+export interface ProfileOptions {
+ mapOptions: MapOptions,
+ shortcuts: {[key: string]: ShortcutOptions}
}
export interface ContextMenuOption {
@@ -284,25 +295,13 @@ export interface AirbaseChartRunwayData {
}
export interface ShortcutOptions {
- altKey?: boolean;
- callback: CallableFunction;
- ctrlKey?: boolean;
- name?: string;
- shiftKey?: boolean;
-}
-
-export interface ShortcutKeyboardOptions extends ShortcutOptions {
+ label: string;
+ keyUpCallback: (e: KeyboardEvent) => void;
+ keyDownCallback?: (e: KeyboardEvent) => void;
code: string;
- event?: "keydown" | "keyup";
-}
-
-export interface ShortcutMouseOptions extends ShortcutOptions {
- button: number;
- event: "mousedown" | "mouseup";
-}
-
-export interface Manager {
- add: CallableFunction;
+ altKey?: boolean;
+ ctrlKey?: boolean;
+ shiftKey?: boolean;
}
export interface ServerStatus {
diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts
index a063d118..8958449b 100644
--- a/frontend/react/src/map/map.ts
+++ b/frontend/react/src/map/map.ts
@@ -22,6 +22,7 @@ import {
UnitControlSubState,
ContextActionTarget,
ContextActionType,
+ ContextActions,
} from "../constants/constants";
import { CoalitionPolygon } from "./coalitionarea/coalitionpolygon";
import { MapHiddenTypes, MapOptions } from "../types/types";
@@ -88,9 +89,7 @@ export class Map extends L.Map {
#panRight: boolean = false;
#panUp: boolean = false;
#panDown: boolean = false;
-
- /* Keyboard state */
- #isShiftKeyDown: boolean = false;
+ #panFast: boolean = false;
/* Center on unit target */
#centeredUnit: Unit | null = null;
@@ -193,9 +192,6 @@ export class Map extends L.Map {
this.on("mousemove", (e: any) => this.#onMouseMove(e));
- this.on("keydown", (e: any) => this.#onKeyDown(e));
- this.on("keyup", (e: any) => this.#onKeyUp(e));
-
this.on("move", (e: any) => this.#onMapMove(e));
/* Custom touch events for touchscreen support */
@@ -239,8 +235,8 @@ export class Map extends L.Map {
let layerSet = false;
/* First load the map mirrors */
- if (config.mapMirrors) {
- let mapMirrors = config.mapMirrors;
+ if (config.frontend.mapMirrors) {
+ let mapMirrors = config.frontend.mapMirrors;
this.#mapMirrors = {
...this.#mapMirrors,
...mapMirrors,
@@ -255,8 +251,8 @@ export class Map extends L.Map {
}
/* Then load the map layers */
- if (config.mapLayers) {
- let mapLayers = config.mapLayers;
+ if (config.frontend.mapLayers) {
+ let mapLayers = config.frontend.mapLayers;
this.#mapLayers = {
...this.#mapLayers,
...mapLayers,
@@ -289,12 +285,118 @@ export class Map extends L.Map {
if (this.#panUp || this.#panDown || this.#panRight || this.#panLeft)
this.panBy(
new L.Point(
- ((this.#panLeft ? -1 : 0) + (this.#panRight ? 1 : 0)) * this.defaultPanDelta * (this.#isShiftKeyDown ? 3 : 1),
- ((this.#panUp ? -1 : 0) + (this.#panDown ? 1 : 0)) * this.defaultPanDelta * (this.#isShiftKeyDown ? 3 : 1)
+ ((this.#panLeft ? -1 : 0) + (this.#panRight ? 1 : 0)) * this.defaultPanDelta * (this.#panFast ? 3 : 1),
+ ((this.#panUp ? -1 : 0) + (this.#panDown ? 1 : 0)) * this.defaultPanDelta * (this.#panFast ? 3 : 1)
)
);
}, 20);
+ getApp()
+ .getShortcutManager()
+ .addShortcut("toggleUnitLabels", {
+ label: "Hide/show labels",
+ keyUpCallback: () => this.setOption("showUnitLabels", !this.getOptions().showUnitLabels),
+ code: "KeyL",
+ })
+ .addShortcut("toggleAcquisitionRings", {
+ label: "Hide/show acquisition rings",
+ keyUpCallback: () => this.setOption("showUnitsAcquisitionRings", !this.getOptions().showUnitsAcquisitionRings),
+ code: "KeyE",
+ })
+ .addShortcut("toggleEngagementRings", {
+ label: "Hide/show engagement rings",
+ keyUpCallback: () => this.setOption("showUnitsEngagementRings", !this.getOptions().showUnitsEngagementRings),
+ code: "KeyQ",
+ })
+ .addShortcut("toggleHideShortEngagementRings", {
+ label: "Hide/show short range rings",
+ keyUpCallback: () => this.setOption("hideUnitsShortRangeRings", !this.getOptions().hideUnitsShortRangeRings),
+ code: "KeyR",
+ })
+ .addShortcut("toggleDetectionLines", {
+ label: "Hide/show detection lines",
+ keyUpCallback: () => this.setOption("showUnitTargets", !this.getOptions().showUnitTargets),
+ code: "KeyF",
+ })
+ .addShortcut("toggleGroupMembers", {
+ label: "Hide/show group members",
+ keyUpCallback: () => this.setOption("hideGroupMembers", !this.getOptions().hideGroupMembers),
+ code: "KeyG",
+ })
+ .addShortcut("toggleRelativePositions", {
+ label: "Toggle group movement mode",
+ keyUpCallback: () => this.setOption("keepRelativePositions", !this.getOptions().keepRelativePositions),
+ code: "KeyP",
+ })
+ .addShortcut("increaseCameraZoom", {
+ label: "Increase camera zoom",
+ altKey: true,
+ keyUpCallback: () => this.increaseCameraZoom(),
+ code: "Equal",
+ })
+ .addShortcut("decreaseCameraZoom", {
+ label: "Decrease camera zoom",
+ altKey: true,
+ keyUpCallback: () => this.decreaseCameraZoom(),
+ code: "Minus",
+ });
+
+ for (let contextActionName in ContextActions) {
+ if (ContextActions[contextActionName].getOptions().hotkey) {
+ getApp()
+ .getShortcutManager()
+ .addShortcut(`${contextActionName}Hotkey`, {
+ label: ContextActions[contextActionName].getLabel(),
+ code: ContextActions[contextActionName].getOptions().hotkey,
+ shiftKey: true,
+ keyUpCallback: () => {
+ const contextActionSet = this.getContextActionSet();
+ if (
+ getApp().getState() === OlympusState.UNIT_CONTROL &&
+ contextActionSet &&
+ ContextActions[contextActionName].getId() in contextActionSet.getContextActions()
+ ) {
+ if (ContextActions[contextActionName].getOptions().executeImmediately) ContextActions[contextActionName].executeCallback();
+ else this.setContextAction(ContextActions[contextActionName]);
+ }
+ },
+ });
+ }
+ }
+
+ /* Map panning shortcuts */
+ getApp()
+ .getShortcutManager()
+ .addShortcut(`panUp`, {
+ label: "Pan map up",
+ keyUpCallback: (ev: KeyboardEvent) => this.#panUp = false,
+ keyDownCallback: (ev: KeyboardEvent) => this.#panUp = true,
+ code: "KeyW",
+ })
+ .addShortcut(`panDown`, {
+ label: "Pan map down",
+ keyUpCallback: (ev: KeyboardEvent) => this.#panDown = false,
+ keyDownCallback: (ev: KeyboardEvent) => this.#panDown = true,
+ code: "KeyS",
+ })
+ .addShortcut(`panLeft`, {
+ label: "Pan map left",
+ keyUpCallback: (ev: KeyboardEvent) => this.#panLeft = false,
+ keyDownCallback: (ev: KeyboardEvent) => this.#panLeft = true,
+ code: "KeyA",
+ })
+ .addShortcut(`panRight`, {
+ label: "Pan map right",
+ keyUpCallback: (ev: KeyboardEvent) => this.#panRight = false,
+ keyDownCallback: (ev: KeyboardEvent) => this.#panRight = true,
+ code: "KeyD",
+ }).addShortcut(`panFast`, {
+ label: "Pan map fast",
+ keyUpCallback: (ev: KeyboardEvent) => this.#panFast = false,
+ keyDownCallback: (ev: KeyboardEvent) => this.#panFast = true,
+ code: "ShiftLeft",
+ });
+
/* Periodically check if the camera control endpoint is available */
this.#cameraControlTimer = window.setInterval(() => {
this.#checkCameraPort();
@@ -715,48 +817,6 @@ export class Map extends L.Map {
return this.#miniMapLayerGroup;
}
- handleMapPanning(e: any) {
- if (e.type === "keyup") {
- switch (e.code) {
- case "KeyA":
- case "ArrowLeft":
- this.#panLeft = false;
- break;
- case "KeyD":
- case "ArrowRight":
- this.#panRight = false;
- break;
- case "KeyW":
- case "ArrowUp":
- this.#panUp = false;
- break;
- case "KeyS":
- case "ArrowDown":
- this.#panDown = false;
- break;
- }
- } else {
- switch (e.code) {
- case "KeyA":
- case "ArrowLeft":
- this.#panLeft = true;
- break;
- case "KeyD":
- case "ArrowRight":
- this.#panRight = true;
- break;
- case "KeyW":
- case "ArrowUp":
- this.#panUp = true;
- break;
- case "KeyS":
- case "ArrowDown":
- this.#panDown = true;
- break;
- }
- }
- }
-
addTemporaryMarker(latlng: L.LatLng, name: string, coalition: string, commandHash?: string) {
var marker = new TemporaryUnitMarker(latlng, name, coalition, commandHash);
marker.addTo(this);
@@ -781,6 +841,11 @@ export class Map extends L.Map {
MapOptionsChangedEvent.dispatch(this.#options);
}
+ setOptions(options) {
+ this.#options = { ...options };
+ MapOptionsChangedEvent.dispatch(this.#options);
+ }
+
getOptions() {
return this.#options;
}
@@ -1089,7 +1154,7 @@ export class Map extends L.Map {
else document.dispatchEvent(new CustomEvent("forceboxselect", { detail: e.originalEvent }));
} else if (getApp().getState() === OlympusState.UNIT_CONTROL) {
if (e.originalEvent.button === 2) {
- if (!getApp().getMap().getContextAction()) {
+ if (!this.getContextAction()) {
getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.MAP_CONTEXT_MENU);
MapContextMenuRequestEvent.dispatch(pressLocation);
}
@@ -1127,14 +1192,6 @@ export class Map extends L.Map {
if (this.#slaveDCSCamera) this.#broadcastPosition();
}
- #onKeyDown(e: any) {
- this.#isShiftKeyDown = e.originalEvent.shiftKey;
- }
-
- #onKeyUp(e: any) {
- this.#isShiftKeyDown = e.originalEvent.shiftKey;
- }
-
#onZoomStart(e: any) {
this.#previousZoom = this.getZoom();
if (this.#centeredUnit != null) this.#panToUnit(this.#centeredUnit);
diff --git a/frontend/react/src/olympusapp.ts b/frontend/react/src/olympusapp.ts
index f7e7c513..62bd3cbb 100644
--- a/frontend/react/src/olympusapp.ts
+++ b/frontend/react/src/olympusapp.ts
@@ -21,8 +21,8 @@ import { ServerManager } from "./server/servermanager";
import { AudioManager } from "./audio/audiomanager";
import { NO_SUBSTATE, OlympusState, OlympusSubState } from "./constants/constants";
-import { AppStateChangedEvent, ConfigLoadedEvent, InfoPopupEvent, SelectedUnitsChangedEvent } from "./events";
-import { OlympusConfig } from "./interfaces";
+import { AppStateChangedEvent, ConfigLoadedEvent, InfoPopupEvent, MapOptionsChangedEvent, SelectedUnitsChangedEvent, ShortcutsChangedEvent } from "./events";
+import { OlympusConfig, ProfileOptions } from "./interfaces";
export var VERSION = "{{OLYMPUS_VERSION_NUMBER}}";
export var IP = window.location.toString();
@@ -34,6 +34,7 @@ export class OlympusApp {
#state: OlympusState = OlympusState.NOT_INITIALIZED;
#subState: OlympusSubState = NO_SUBSTATE;
#infoMessages: string[] = [];
+ #profileName: string | null = null;
/* Main leaflet map, extended by custom methods */
#map: Map | null = null;
@@ -52,6 +53,9 @@ export class OlympusApp {
if (selectedUnits.length > 0) this.setState(OlympusState.UNIT_CONTROL);
else this.getState() === OlympusState.UNIT_CONTROL && this.setState(OlympusState.IDLE);
});
+
+ MapOptionsChangedEvent.on((options) => getApp().saveProfile());
+ ShortcutsChangedEvent.on((options) => getApp().saveProfile());
}
getMap() {
@@ -90,11 +94,12 @@ export class OlympusApp {
start() {
/* Initialize base functionalitites */
+ this.#shortcutManager = new ShortcutManager(); /* Keep first */
+
this.#map = new Map("map-container");
this.#missionManager = new MissionManager();
this.#serverManager = new ServerManager();
- this.#shortcutManager = new ShortcutManager();
this.#unitsManager = new UnitsManager();
this.#weaponsManager = new WeaponsManager();
this.#audioManager = new AudioManager();
@@ -143,6 +148,54 @@ export class OlympusApp {
return this.#config;
}
+ setProfile(profileName: string) {
+ this.#profileName = profileName;
+ }
+
+ saveProfile() {
+ if (this.#profileName !== null) {
+ let profile = {};
+ profile["mapOptions"] = this.#map?.getOptions();
+ profile["shortcuts"] = this.#shortcutManager?.getShortcutsOptions();
+
+ const requestOptions = {
+ method: "PUT", // Specify the request method
+ headers: { "Content-Type": "application/json" }, // Specify the content type
+ body: JSON.stringify(profile), // Send the data in JSON format
+ };
+
+ fetch(window.location.href.split("?")[0].replace("vite/", "") + `resources/profile/${this.#profileName}`, requestOptions)
+ .then((response) => {
+ if (response.status === 200) {
+ console.log(`Profile ${this.#profileName} saved correctly`);
+ } else {
+ this.addInfoMessage("Error saving profile");
+ throw new Error("Error saving profile file");
+ }
+ }) // Parse the response as JSON
+ .catch((error) => console.error(error)); // Handle errors
+ }
+ }
+
+ getProfile() {
+ if (this.#profileName && this.#config?.profiles && this.#config?.profiles[this.#profileName])
+ return this.#config?.profiles[this.#profileName] as ProfileOptions;
+ else return null;
+ }
+
+ loadProfile() {
+ const profile = this.getProfile();
+ if (profile) {
+ this.#map?.setOptions(profile.mapOptions);
+ this.#shortcutManager?.setShortcutsOptions(profile.shortcuts);
+ this.addInfoMessage("Profile loaded correctly");
+ console.log(`Profile ${this.#profileName} saved correctly`);
+ } else {
+ this.addInfoMessage("Error loading profile");
+ console.log(`Error loading profile`);
+ }
+ }
+
setState(state: OlympusState, subState: OlympusSubState = NO_SUBSTATE) {
if (state !== this.#state || subState !== this.#subState) {
this.#state = state;
diff --git a/frontend/react/src/server/servermanager.ts b/frontend/react/src/server/servermanager.ts
index f86c2437..a1f5a370 100644
--- a/frontend/react/src/server/servermanager.ts
+++ b/frontend/react/src/server/servermanager.ts
@@ -14,7 +14,7 @@ import {
reactionsToThreat,
} from "../constants/constants";
import { AirbasesData, BullseyesData, CommandModeOptions, GeneralSettings, MissionData, Radio, ServerRequestOptions, ServerStatus, TACAN } from "../interfaces";
-import { InfoPopupEvent, ServerStatusUpdatedEvent } from "../events";
+import { ServerStatusUpdatedEvent } from "../events";
export class ServerManager {
#connected: boolean = false;
@@ -36,6 +36,14 @@ export class ServerManager {
this.#lastUpdateTimes[AIRBASES_URI] = Date.now();
this.#lastUpdateTimes[BULLSEYE_URI] = Date.now();
this.#lastUpdateTimes[MISSION_URI] = Date.now();
+
+ getApp().getShortcutManager().addShortcut("togglePause", {
+ label: "Pause data update",
+ callback: () => {
+ this.setPaused(!this.getPaused());
+ },
+ code: "Space"
+ })
}
setUsername(newUsername: string) {
diff --git a/frontend/react/src/shortcut/shortcut.ts b/frontend/react/src/shortcut/shortcut.ts
index fc65efbd..23701958 100644
--- a/frontend/react/src/shortcut/shortcut.ts
+++ b/frontend/react/src/shortcut/shortcut.ts
@@ -1,46 +1,49 @@
-import { getApp } from "../olympusapp";
-import { ShortcutKeyboardOptions, ShortcutMouseOptions, ShortcutOptions } from "../interfaces";
+import { ShortcutChangedEvent, ShortcutsChangedEvent } from "../events";
+import { ShortcutOptions } from "../interfaces";
import { keyEventWasInInput } from "../other/utils";
-export abstract class Shortcut {
- #config: ShortcutOptions;
+export class Shortcut {
+ #id: string;
+ #options: ShortcutOptions;
- constructor(config: ShortcutOptions) {
- this.#config = config;
- }
-
- getConfig() {
- return this.#config;
- }
-}
-
-export class ShortcutKeyboard extends Shortcut {
- constructor(config: ShortcutKeyboardOptions) {
- config.event = config.event || "keyup";
- super(config);
-
- document.addEventListener(config.event, (ev: any) => {
- if (ev instanceof KeyboardEvent === false || keyEventWasInInput(ev)) {
- return;
- }
-
- if (config.code !== ev.code) {
- return;
- }
+ constructor(id, options: ShortcutOptions) {
+ this.#id = id;
+ this.#options = options;
+ /* Key up event is mandatory */
+ document.addEventListener("keyup", (ev: any) => {
+ if (keyEventWasInInput(ev) || options.code !== ev.code) return;
if (
- (typeof config.altKey !== "boolean" || (typeof config.altKey === "boolean" && ev.altKey === config.altKey)) &&
- (typeof config.ctrlKey !== "boolean" || (typeof config.ctrlKey === "boolean" && ev.ctrlKey === config.ctrlKey)) &&
- (typeof config.shiftKey !== "boolean" || (typeof config.shiftKey === "boolean" && ev.shiftKey === config.shiftKey))
- ) {
- config.callback(ev);
- }
+ (typeof options.altKey !== "boolean" || (typeof options.altKey === "boolean" && ev.altKey === options.altKey)) &&
+ (typeof options.ctrlKey !== "boolean" || (typeof options.ctrlKey === "boolean" && ev.ctrlKey === options.ctrlKey)) &&
+ (typeof options.shiftKey !== "boolean" || (typeof options.shiftKey === "boolean" && ev.shiftKey === options.shiftKey))
+ )
+ options.keyUpCallback(ev);
});
- }
-}
-export class ShortcutMouse extends Shortcut {
- constructor(config: ShortcutMouseOptions) {
- super(config);
+ /* Key down event is optional */
+ if (options.keyDownCallback) {
+ document.addEventListener("keydown", (ev: any) => {
+ if (keyEventWasInInput(ev) || options.code !== ev.code) return;
+ if (
+ (typeof options.altKey !== "boolean" || (typeof options.altKey === "boolean" && ev.altKey === options.altKey)) &&
+ (typeof options.ctrlKey !== "boolean" || (typeof options.ctrlKey === "boolean" && ev.ctrlKey === options.ctrlKey)) &&
+ (typeof options.shiftKey !== "boolean" || (typeof options.shiftKey === "boolean" && ev.shiftKey === options.shiftKey))
+ )
+ if (options.keyDownCallback) options.keyDownCallback(ev);
+ });
+ }
+ }
+
+ getOptions() {
+ return this.#options;
+ }
+
+ setOptions(options: ShortcutOptions) {
+ this.#options = { ...options };
+ }
+
+ getId() {
+ return this.#id;
}
}
diff --git a/frontend/react/src/shortcut/shortcutmanager.ts b/frontend/react/src/shortcut/shortcutmanager.ts
index 07c882fb..74659ea4 100644
--- a/frontend/react/src/shortcut/shortcutmanager.ts
+++ b/frontend/react/src/shortcut/shortcutmanager.ts
@@ -1,252 +1,42 @@
-import { ContextActions, OlympusState } from "../constants/constants";
-import { ShortcutKeyboardOptions, ShortcutMouseOptions } from "../interfaces";
-import { getApp } from "../olympusapp";
-import { ShortcutKeyboard, ShortcutMouse } from "./shortcut";
+import { ShortcutChangedEvent, ShortcutsChangedEvent } from "../events";
+import { ShortcutOptions } from "../interfaces";
+import { Shortcut } from "./shortcut";
export class ShortcutManager {
- #items: { [key: string]: any } = {};
- #keysBeingHeld: string[] = [];
- #keyDownCallbacks: CallableFunction[] = [];
- #keyUpCallbacks: CallableFunction[] = [];
+ #shortcuts: { [key: string]: Shortcut } = {};
constructor() {
+ // Stop ctrl+digits from sending the browser to another tab
document.addEventListener("keydown", (ev: KeyboardEvent) => {
- if (this.#keysBeingHeld.indexOf(ev.code) < 0) {
- this.#keysBeingHeld.push(ev.code);
+ if (ev.code.indexOf("Digit") >= 0 && ev.ctrlKey === true && ev.altKey === false && ev.shiftKey === false) {
+ ev.preventDefault();
}
- this.#keyDownCallbacks.forEach((callback) => callback(ev));
- });
-
- document.addEventListener("keyup", (ev: KeyboardEvent) => {
- this.#keysBeingHeld = this.#keysBeingHeld.filter((held) => held !== ev.code);
- this.#keyUpCallbacks.forEach((callback) => callback(ev));
- });
-
- this.addKeyboardShortcut("togglePause", {
- altKey: false,
- callback: () => {
- getApp().getServerManager().setPaused(!getApp().getServerManager().getPaused());
- },
- code: "Space",
- ctrlKey: false,
- })
- .addKeyboardShortcut("deselectAll", {
- callback: (ev: KeyboardEvent) => {
- getApp().getUnitsManager().deselectAllUnits();
- },
- code: "Escape",
- })
- .addKeyboardShortcut("toggleUnitLabels", {
- altKey: false,
- callback: () => {
- getApp().getMap().setOption("showUnitLabels", !getApp().getMap().getOptions().showUnitLabels);
- },
- code: "KeyL",
- ctrlKey: false,
- shiftKey: false,
- })
- .addKeyboardShortcut("toggleAcquisitionRings", {
- altKey: false,
- callback: () => {
- getApp().getMap().setOption("showUnitsAcquisitionRings", !getApp().getMap().getOptions().showUnitsAcquisitionRings);
- },
- code: "KeyE",
- ctrlKey: false,
- shiftKey: false,
- })
- .addKeyboardShortcut("toggleEngagementRings", {
- altKey: false,
- callback: () => {
- getApp().getMap().setOption("showUnitsEngagementRings", !getApp().getMap().getOptions().showUnitsEngagementRings);
- },
- code: "KeyQ",
- ctrlKey: false,
- shiftKey: false,
- })
- .addKeyboardShortcut("toggleHideShortEngagementRings", {
- altKey: false,
- callback: () => {
- getApp().getMap().setOption("hideUnitsShortRangeRings", !getApp().getMap().getOptions().hideUnitsShortRangeRings);
- },
- code: "KeyR",
- ctrlKey: false,
- shiftKey: false,
- })
- .addKeyboardShortcut("toggleDetectionLines", {
- altKey: false,
- callback: () => {
- getApp().getMap().setOption("showUnitTargets", !getApp().getMap().getOptions().showUnitTargets);
- },
- code: "KeyF",
- ctrlKey: false,
- shiftKey: false,
- })
- .addKeyboardShortcut("toggleGroupMembers", {
- altKey: false,
- callback: () => {
- getApp().getMap().setOption("hideGroupMembers", !getApp().getMap().getOptions().hideGroupMembers);
- },
- code: "KeyG",
- ctrlKey: false,
- shiftKey: false,
- })
- .addKeyboardShortcut("toggleRelativePositions", {
- altKey: false,
- callback: () => {
- getApp().getMap().setOption("keepRelativePositions", !getApp().getMap().getOptions().keepRelativePositions);
- },
- code: "KeyP",
- ctrlKey: false,
- shiftKey: false,
- })
- .addKeyboardShortcut("increaseCameraZoom", {
- altKey: true,
- callback: () => {
- //getApp().getMap().increaseCameraZoom();
- },
- code: "Equal",
- ctrlKey: false,
- shiftKey: false,
- })
- .addKeyboardShortcut("decreaseCameraZoom", {
- altKey: true,
- callback: () => {
- //getApp().getMap().decreaseCameraZoom();
- },
- code: "Minus",
- ctrlKey: false,
- shiftKey: false,
- });
-
- for (let contextActionName in ContextActions) {
- if (ContextActions[contextActionName].getOptions().hotkey) {
- this.addKeyboardShortcut(`${contextActionName}Hotkey`, {
- code: ContextActions[contextActionName].getOptions().hotkey,
- shiftKey: true,
- callback: () => {
- const contextActionSet = getApp().getMap().getContextActionSet();
- if (
- getApp().getState() === OlympusState.UNIT_CONTROL &&
- contextActionSet &&
- ContextActions[contextActionName].getId() in contextActionSet.getContextActions()
- ) {
- if (ContextActions[contextActionName].getOptions().executeImmediately) ContextActions[contextActionName].executeCallback();
- else getApp().getMap().setContextAction(ContextActions[contextActionName]);
- }
- },
- });
- }
- }
-
- let PTTKeys = ["KeyZ", "KeyX", "KeyC", "KeyV", "KeyB", "KeyN", "KeyM", "KeyK", "KeyL"];
- PTTKeys.forEach((key, idx) => {
- this.addKeyboardShortcut(`PTT${idx}Active`, {
- altKey: false,
- callback: () => {
- getApp().getAudioManager().getSinks()[idx]?.setPtt(true);
- },
- code: key,
- ctrlKey: false,
- shiftKey: false,
- event: "keydown",
- }).addKeyboardShortcut(`PTT${idx}Active`, {
- altKey: false,
- callback: () => {
- getApp().getAudioManager().getSinks()[idx]?.setPtt(false);
- },
- code: key,
- ctrlKey: false,
- shiftKey: false,
- event: "keyup",
- });
- });
-
- let panKeys = ["KeyW", "KeyA", "KeyS", "KeyD", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"];
- panKeys.forEach((code) => {
- this.addKeyboardShortcut(`pan${code}keydown`, {
- altKey: false,
- callback: (ev: KeyboardEvent) => {
- getApp().getMap().handleMapPanning(ev);
- },
- code: code,
- ctrlKey: false,
- event: "keydown",
- });
-
- this.addKeyboardShortcut(`pan${code}keyup`, {
- callback: (ev: KeyboardEvent) => {
- getApp().getMap().handleMapPanning(ev);
- },
- code: code,
- });
- });
-
- const digits = ["Digit1", "Digit2", "Digit3", "Digit4", "Digit5", "Digit6", "Digit7", "Digit8", "Digit9"];
-
- digits.forEach((code) => {
- this.addKeyboardShortcut(`hotgroup${code}`, {
- altKey: false,
- callback: (ev: KeyboardEvent) => {
- if (ev.ctrlKey && ev.shiftKey)
- getApp()
- .getUnitsManager()
- .selectUnitsByHotgroup(parseInt(ev.code.substring(5)), false);
- // "Select hotgroup X in addition to any units already selected"
- else if (ev.ctrlKey && !ev.shiftKey)
- getApp()
- .getUnitsManager()
- .setHotgroup(parseInt(ev.code.substring(5)));
- // "These selected units are hotgroup X (forget any previous membership)"
- else if (!ev.ctrlKey && ev.shiftKey)
- getApp()
- .getUnitsManager()
- .addToHotgroup(parseInt(ev.code.substring(5)));
- // "Add (append) these units to hotgroup X (in addition to any existing members)"
- else
- getApp()
- .getUnitsManager()
- .selectUnitsByHotgroup(parseInt(ev.code.substring(5))); // "Select hotgroup X, deselect any units not in it."
- },
- code: code,
- });
-
- // Stop hotgroup controls sending the browser to another tab
- document.addEventListener("keydown", (ev: KeyboardEvent) => {
- if (ev.code === code && ev.ctrlKey === true && ev.altKey === false && ev.shiftKey === false) {
- ev.preventDefault();
- }
- });
});
}
- addKeyboardShortcut(name: string, shortcutKeyboardOptions: ShortcutKeyboardOptions) {
- this.#items[name] = new ShortcutKeyboard(shortcutKeyboardOptions);
+ addShortcut(id: string, shortcutOptions: ShortcutOptions) {
+ this.#shortcuts[id] = new Shortcut(id, shortcutOptions);
+ ShortcutsChangedEvent.dispatch(this.#shortcuts);
return this;
}
- addMouseShortcut(name: string, shortcutMouseOptions: ShortcutMouseOptions) {
- this.#items[name] = new ShortcutMouse(shortcutMouseOptions);
- return this;
- }
-
- getKeysBeingHeld() {
- return this.#keysBeingHeld;
- }
-
- keyComboMatches(combo: string[]) {
- const heldKeys = this.getKeysBeingHeld();
- if (combo.length !== heldKeys.length) {
- return false;
+ getShortcutsOptions() {
+ let shortcutsOptions = {};
+ for (let id in this.#shortcuts) {
+ shortcutsOptions[id] = this.#shortcuts[id].getOptions();
}
-
- return combo.every((key) => heldKeys.indexOf(key) > -1);
+ return shortcutsOptions;
}
- onKeyDown(callback: CallableFunction) {
- this.#keyDownCallbacks.push(callback);
+ setShortcutsOptions(shortcutOptions: { [key: string]: ShortcutOptions }) {
+ for (let id in shortcutOptions) {
+ this.#shortcuts[id].setOptions(shortcutOptions[id]);
+ }
+ ShortcutsChangedEvent.dispatch(this.#shortcuts);
}
- onKeyUp(callback: CallableFunction) {
- this.#keyUpCallbacks.push(callback);
+ setShortcutOption(id: string, shortcutOptions: ShortcutOptions) {
+ this.#shortcuts[id].setOptions(shortcutOptions);
+ ShortcutsChangedEvent.dispatch(this.#shortcuts);
}
}
diff --git a/frontend/react/src/ui/modals/keybindmodal.tsx b/frontend/react/src/ui/modals/keybindmodal.tsx
new file mode 100644
index 00000000..41b41401
--- /dev/null
+++ b/frontend/react/src/ui/modals/keybindmodal.tsx
@@ -0,0 +1,151 @@
+import React, { useEffect, useState } from "react";
+import { Modal } from "./components/modal";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
+import { getApp } from "../../olympusapp";
+import { OlympusState } from "../../constants/constants";
+import { Shortcut } from "../../shortcut/shortcut";
+import { BindShortcutRequestEvent, ShortcutsChangedEvent } from "../../events";
+
+export function KeybindModal(props: { open: boolean }) {
+ const [shortcuts, setShortcuts] = useState({} as { [key: string]: Shortcut });
+ const [shortcut, setShortcut] = useState(null as null | Shortcut);
+ const [code, setCode] = useState(null as null | string);
+ const [shiftKey, setShiftKey] = useState(false);
+ const [ctrlKey, setCtrlKey] = useState(false);
+ const [altKey, setAltKey] = useState(false);
+
+ useEffect(() => {
+ ShortcutsChangedEvent.on((shortcuts) => setShortcuts({ ...shortcuts }));
+ BindShortcutRequestEvent.on((shortcut) => setShortcut(shortcut));
+
+ document.addEventListener("keydown", (ev) => {
+ setCode(ev.code);
+ if (!(ev.code.indexOf("Shift") >= 0 || ev.code.indexOf("Alt") >= 0 || ev.code.indexOf("Control") >= 0)) {
+ setShiftKey(ev.shiftKey);
+ setAltKey(ev.altKey);
+ setCtrlKey(ev.ctrlKey);
+ }
+ });
+ }, []);
+
+ let available: null | boolean = code ? true : null;
+ let inUseShortcut: null | Shortcut = null;
+ for (let id in shortcuts) {
+ if (
+ code === shortcuts[id].getOptions().code &&
+ shiftKey == shortcuts[id].getOptions().shiftKey &&
+ altKey == shortcuts[id].getOptions().altKey &&
+ ctrlKey == shortcuts[id].getOptions().shiftKey
+ ) {
+ available = false;
+ inUseShortcut = shortcuts[id];
+ }
+ }
+
+ return (
+ <>
+ {props.open && (
+ <>
+