diff --git a/frontend/react/src/audio/audiomanager.ts b/frontend/react/src/audio/audiomanager.ts index 7a9f5018..928172b7 100644 --- a/frontend/react/src/audio/audiomanager.ts +++ b/frontend/react/src/audio/audiomanager.ts @@ -51,7 +51,9 @@ export class AudioManager { keyDownCallback: () => this.getSinks()[idx]?.setPtt(true), keyUpCallback: () => this.getSinks()[idx]?.setPtt(false), code: key, - shiftKey: true + shiftKey: true, + ctrlKey: false, + altKey: false }); }); } diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts index 284171db..826387a7 100644 --- a/frontend/react/src/constants/constants.ts +++ b/frontend/react/src/constants/constants.ts @@ -25,6 +25,10 @@ import { } from "../ui/components/olicons"; import { FormationCreationRequestEvent, UnitExplosionRequestEvent } from "../events"; +export const SELECT_TOLERANCE_PX = 5; +export const SHORT_PRESS_MILLISECONDS = 200; +export const DEBOUNCE_MILLISECONDS = 200; + export const UNITS_URI = "units"; export const WEAPONS_URI = "weapons"; export const LOGS_URI = "logs"; @@ -73,8 +77,8 @@ export enum ERAS_ORDER { "Early Cold War", "Mid Cold War", "Late Cold War", - "Modern" -}; + "Modern", +} export const ROEDescriptions: string[] = [ "Free (Attack anyone)", @@ -331,12 +335,11 @@ export const MAP_OPTIONS_DEFAULTS: MapOptions = { fillSelectedRing: false, showMinimap: false, protectDCSUnits: true, - keepRelativePositions: true, cameraPluginPort: 3003, cameraPluginRatio: 1, cameraPluginEnabled: false, cameraPluginMode: "map", - tabletMode: false + tabletMode: false, }; export const MAP_HIDDEN_TYPES_DEFAULTS = { @@ -473,7 +476,7 @@ export namespace ContextActions { if (targetPosition) getApp() .getUnitsManager() - .addDestination(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units); + .addDestination(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units); }, { type: ContextActionType.MOVE, code: null } ); @@ -488,9 +491,9 @@ export namespace ContextActions { if (targetPosition) getApp() .getUnitsManager() - .addDestination(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units); + .addDestination(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units); }, - { type: ContextActionType.MOVE, code: "ControlLeft" } + { type: ContextActionType.MOVE, code: "ControlLeft", shiftKey: false } ); export const DELETE = new ContextAction( @@ -505,7 +508,10 @@ export namespace ContextActions { { executeImmediately: true, type: ContextActionType.DELETE, - code: "Delete" + code: "Delete", + ctrlKey: false, + shiftKey: false, + altKey: false, } ); @@ -523,7 +529,9 @@ export namespace ContextActions { executeImmediately: true, type: ContextActionType.DELETE, code: "Delete", - ctrlKey: true + ctrlKey: true, + shiftKey: false, + altKey: false, } ); @@ -536,7 +544,7 @@ export namespace ContextActions { (units: Unit[]) => { getApp().getMap().centerOnUnit(units[0]); }, - { executeImmediately: true, type: ContextActionType.OTHER, code: "KeyM", altKey: true } + { executeImmediately: true, type: ContextActionType.OTHER, code: "KeyM", ctrlKey: false, shiftKey: false, altKey: false } ); export const REFUEL = new ContextAction( @@ -548,7 +556,7 @@ export namespace ContextActions { (units: Unit[]) => { getApp().getUnitsManager().refuel(units); }, - { executeImmediately: true, type: ContextActionType.ADMIN, code: "KeyV" } + { executeImmediately: true, type: ContextActionType.ADMIN, code: "KeyR", ctrlKey: false, shiftKey: false, altKey: false } ); export const FOLLOW = new ContextAction( @@ -566,7 +574,7 @@ export namespace ContextActions { ); } }, - { type: ContextActionType.ADMIN, code: "KeyF" } + { type: ContextActionType.ADMIN, code: "KeyF", ctrlKey: false, shiftKey: false, altKey: false } ); export const BOMB = new ContextAction( @@ -579,9 +587,9 @@ export namespace ContextActions { if (targetPosition) getApp() .getUnitsManager() - .bombPoint(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units); + .bombPoint(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units); }, - { type: ContextActionType.ENGAGE, code: "KeyB" } + { type: ContextActionType.ENGAGE, code: "KeyB", ctrlKey: false, shiftKey: false } ); export const CARPET_BOMB = new ContextAction( @@ -594,9 +602,9 @@ export namespace ContextActions { if (targetPosition) getApp() .getUnitsManager() - .carpetBomb(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units); + .carpetBomb(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units); }, - { type: ContextActionType.ENGAGE, code: "KeyB", altKey: true } + { type: ContextActionType.ENGAGE, code: "KeyC", ctrlKey: false, shiftKey: false } ); export const LAND = new ContextAction( @@ -608,7 +616,7 @@ export namespace ContextActions { (units: Unit[], _, targetPosition: LatLng | null) => { if (targetPosition) getApp().getUnitsManager().landAt(targetPosition, units); }, - { type: ContextActionType.ADMIN, code: "KeyL" } + { type: ContextActionType.ADMIN, code: "KeyL", ctrlKey: false, shiftKey: false } ); export const LAND_AT_POINT = new ContextAction( @@ -621,9 +629,9 @@ export namespace ContextActions { if (targetPosition) getApp() .getUnitsManager() - .landAtPoint(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units); + .landAtPoint(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units); }, - { type: ContextActionType.ADMIN, code: "KeyL", altKey: true } + { type: ContextActionType.ADMIN, code: "KeyK", ctrlKey: false, shiftKey: false } ); export const GROUP = new ContextAction( @@ -635,7 +643,7 @@ export namespace ContextActions { (units: Unit[], _1, _2) => { getApp().getUnitsManager().createGroup(units); }, - { executeImmediately: true, type: ContextActionType.OTHER, code: "KeyG" } + { executeImmediately: true, type: ContextActionType.OTHER, code: "KeyG", ctrlKey: false, shiftKey: false, altKey: false } ); export const ATTACK = new ContextAction( @@ -647,7 +655,7 @@ export namespace ContextActions { (units: Unit[], targetUnit: Unit | null, _) => { if (targetUnit) getApp().getUnitsManager().attackUnit(targetUnit.ID, units); }, - { type: ContextActionType.ENGAGE, code: "KeyZ" } + { type: ContextActionType.ENGAGE, code: "KeyZ", ctrlKey: false, shiftKey: false, altKey: false } ); export const FIRE_AT_AREA = new ContextAction( @@ -660,9 +668,9 @@ export namespace ContextActions { if (targetPosition) getApp() .getUnitsManager() - .fireAtArea(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units); + .fireAtArea(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units); }, - { type: ContextActionType.ENGAGE, code: "KeyZ", altKey: true } + { type: ContextActionType.ENGAGE, code: "KeyV", ctrlKey: false, shiftKey: false } ); export const SIMULATE_FIRE_FIGHT = new ContextAction( @@ -675,8 +683,8 @@ export namespace ContextActions { if (targetPosition) getApp() .getUnitsManager() - .simulateFireFight(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units); + .simulateFireFight(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units); }, - { type: ContextActionType.ADMIN, code: "KeyX" } + { type: ContextActionType.ADMIN, code: "KeyX", ctrlKey: false, shiftKey: false } ); } diff --git a/frontend/react/src/events.ts b/frontend/react/src/events.ts index 1df318c4..9c8b3421 100644 --- a/frontend/react/src/events.ts +++ b/frontend/react/src/events.ts @@ -389,7 +389,7 @@ export class CommandModeOptionsChangedEvent { export class AudioSourcesChangedEvent { static on(callback: (audioSources: AudioSource[]) => void) { document.addEventListener(this.name, (ev: CustomEventInit) => { - callback(ev.detail); + callback(ev.detail.audioSources); }); } @@ -403,7 +403,7 @@ export class AudioSourcesChangedEvent { export class AudioSinksChangedEvent { static on(callback: (audioSinks: AudioSink[]) => void) { document.addEventListener(this.name, (ev: CustomEventInit) => { - callback(ev.detail); + callback(ev.detail.audioSinks); }); } diff --git a/frontend/react/src/map/boxselect.ts b/frontend/react/src/map/boxselect.ts index a5f09215..d525014a 100644 --- a/frontend/react/src/map/boxselect.ts +++ b/frontend/react/src/map/boxselect.ts @@ -4,6 +4,7 @@ import { DomUtil } from "leaflet"; import { DomEvent } from "leaflet"; import { LatLngBounds } from "leaflet"; import { Bounds } from "leaflet"; +import { SELECT_TOLERANCE_PX } from "../constants/constants"; export var BoxSelect = Handler.extend({ initialize: function (map) { @@ -11,24 +12,15 @@ export var BoxSelect = Handler.extend({ this._container = map.getContainer(); this._pane = map.getPanes().overlayPane; this._resetStateTimeout = 0; - this._forceBoxSelect = false; map.on("unload", this._destroy, this); - - document.addEventListener("forceboxselect", (e) => { - this._forceBoxSelect = true; - const originalEvent = (e as CustomEvent).detail; - this._onMouseDown(originalEvent); - }); }, addHooks: function () { DomEvent.on(this._container, "mousedown", this._onMouseDown, this); - DomEvent.on(this._container, "forceboxselect", this._onMouseDown, this); }, removeHooks: function () { DomEvent.off(this._container, "mousedown", this._onMouseDown, this); - DomEvent.off(this._container, "forceboxselect", this._onMouseDown, this); }, moved: function () { @@ -53,8 +45,7 @@ export var BoxSelect = Handler.extend({ }, _onMouseDown: function (e: any) { - if ((e.which == 1 && e.button == 0 && (e.shiftKey || this._forceBoxSelect)) || (e.type === "touchstart" && this._forceBoxSelect)) { - this._map.fire("selectionstart"); + if (e.which == 1 && e.button == 0) { // Clear the deferred resetState if it hasn't executed yet, otherwise it // will interrupt the interaction and orphan a box element in the container. this._clearDeferredResetState(); @@ -87,15 +78,23 @@ export var BoxSelect = Handler.extend({ }, _onMouseMove: function (e: any) { + if (e.type === "touchmove") this._point = this._map.mouseEventToContainerPoint(e.touches[0]); + else this._point = this._map.mouseEventToContainerPoint(e); + + if ( + Math.abs(this._startPoint.x - this._point.x) < SELECT_TOLERANCE_PX && + Math.abs(this._startPoint.y - this._point.y) < SELECT_TOLERANCE_PX && + !this._moved + ) + return; + if (!this._moved) { + this._map.fire("selectionstart"); this._moved = true; this._box = DomUtil.create("div", "leaflet-zoom-box", this._container); DomUtil.addClass(this._container, "leaflet-crosshair"); } - if (e.type === "touchmove") this._point = this._map.mouseEventToContainerPoint(e.touches[0]); - else this._point = this._map.mouseEventToContainerPoint(e); - var bounds = new Bounds(this._point, this._startPoint), size = bounds.getSize(); @@ -114,7 +113,6 @@ export var BoxSelect = Handler.extend({ DomUtil.enableTextSelection(); DomUtil.enableImageDrag(); this._map.dragging.enable(); - this._forceBoxSelect = false; DomEvent.off( //@ts-ignore diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index 0a6efce3..dd4cc8a3 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -23,6 +23,8 @@ import { ContextActionTarget, ContextActionType, ContextActions, + SHORT_PRESS_MILLISECONDS, + DEBOUNCE_MILLISECONDS, } from "../constants/constants"; import { CoalitionPolygon } from "./coalitionarea/coalitionpolygon"; import { MapHiddenTypes, MapOptions } from "../types/types"; @@ -99,11 +101,19 @@ export class Map extends L.Map { /* Other state controls */ #isZooming: boolean = false; #isDragging: boolean = false; - #isMouseDown: boolean = false; + #isSelecting: boolean = false; + + #debounceTimeout: number | null = null; + #isLeftMouseDown: boolean = false; + #isRightMouseDown: boolean = false; + #leftMouseDownEpoch: number = 0; + #rightMouseDownEpoch: number = 0; + #leftMouseDownTimeout: number = 0; + #rightMouseDownTimeout: number = 0; #lastMousePosition: L.Point = new L.Point(0, 0); #lastMouseCoordinates: L.LatLng = new L.LatLng(0, 0); #previousZoom: number = 0; - #selecting: boolean = false; + #keepRelativePositions: boolean = false; /* Camera control plugin */ #slaveDCSCamera: boolean = false; @@ -122,6 +132,7 @@ export class Map extends L.Map { #destinationPreviewMarkers: { [key: number]: TemporaryUnitMarker | TargetMarker } = {}; #destinationRotation: number = 0; #isRotatingDestination: boolean = false; + #destionationWasRotated: boolean = false; /* Unit context actions */ #contextActionSet: null | ContextActionSet = null; @@ -185,8 +196,9 @@ export class Map extends L.Map { this.on("mouseup", (e: any) => this.#onMouseUp(e)); this.on("mousedown", (e: any) => this.#onMouseDown(e)); - this.on("click", (e: any) => this.#onLeftClick(e)); - this.on("contextmenu", (e: any) => this.#onRightClick(e)); + this.on("dblclick", (e: any) => this.#onDoubleClick(e)); + this.on("click", (e: any) => e.originalEvent.preventDefault()); + this.on("contextmenu", (e: any) => e.originalEvent.preventDefault()); this.on("mousemove", (e: any) => this.#onMouseMove(e)); @@ -195,6 +207,7 @@ export class Map extends L.Map { /* Custom touch events for touchscreen support */ L.DomEvent.on(this.getContainer(), "touchstart", this.#onMouseDown, this); L.DomEvent.on(this.getContainer(), "touchend", this.#onMouseUp, this); + L.DomEvent.on(this.getContainer(), 'wheel', this.#onMouseWheel, this); /* Event listeners */ AppStateChangedEvent.on((state, subState) => this.#onStateChanged(state, subState)); @@ -295,55 +308,73 @@ export class Map extends L.Map { label: "Hide/show labels", keyUpCallback: () => this.setOption("showUnitLabels", !this.getOptions().showUnitLabels), code: "KeyL", - shiftKey: true + shiftKey: true, + altKey: false, + ctrlKey: false, }) .addShortcut("toggleAcquisitionRings", { label: "Hide/show acquisition rings", keyUpCallback: () => this.setOption("showUnitsAcquisitionRings", !this.getOptions().showUnitsAcquisitionRings), code: "KeyE", - shiftKey: true + shiftKey: true, + altKey: false, + ctrlKey: false, }) .addShortcut("toggleEngagementRings", { label: "Hide/show engagement rings", keyUpCallback: () => this.setOption("showUnitsEngagementRings", !this.getOptions().showUnitsEngagementRings), code: "KeyQ", - shiftKey: true + shiftKey: true, + altKey: false, + ctrlKey: false, }) .addShortcut("toggleHideShortEngagementRings", { label: "Hide/show short range rings", keyUpCallback: () => this.setOption("hideUnitsShortRangeRings", !this.getOptions().hideUnitsShortRangeRings), code: "KeyR", - shiftKey: true + shiftKey: true, + altKey: false, + ctrlKey: false, }) .addShortcut("toggleDetectionLines", { label: "Hide/show detection lines", keyUpCallback: () => this.setOption("showUnitTargets", !this.getOptions().showUnitTargets), code: "KeyF", - shiftKey: true + shiftKey: true, + altKey: false, + ctrlKey: false, }) .addShortcut("toggleGroupMembers", { label: "Hide/show group members", keyUpCallback: () => this.setOption("hideGroupMembers", !this.getOptions().hideGroupMembers), code: "KeyG", - shiftKey: true + shiftKey: true, + altKey: false, + ctrlKey: false, }) .addShortcut("toggleRelativePositions", { label: "Toggle group movement mode", - keyUpCallback: () => this.setOption("keepRelativePositions", !this.getOptions().keepRelativePositions), - code: "KeyP", - shiftKey: true + keyUpCallback: () => this.setKeepRelativePositions(false), + keyDownCallback: () => this.setKeepRelativePositions(true), + code: "AltLeft", + shiftKey: false, + ctrlKey: false, }) .addShortcut("increaseCameraZoom", { label: "Increase camera zoom", keyUpCallback: () => this.increaseCameraZoom(), code: "Equal", - shiftKey: true + shiftKey: true, + altKey: false, + ctrlKey: false, }) .addShortcut("decreaseCameraZoom", { label: "Decrease camera zoom", keyUpCallback: () => this.decreaseCameraZoom(), code: "Minus", - shiftKey: true + shiftKey: true, + altKey: false, + ctrlKey: false, }); for (let contextActionName in ContextActions) { @@ -357,15 +388,14 @@ export class Map extends L.Map { shiftKey: contextAction.getOptions().shiftKey, altKey: contextAction.getOptions().altKey, ctrlKey: contextAction.getOptions().ctrlKey, - keyDownCallback: () => { - const contextActionSet = this.getContextActionSet(); - if (getApp().getState() === OlympusState.UNIT_CONTROL && contextActionSet && contextAction.getId() in contextActionSet.getContextActions()) { - if (contextAction.getOptions().executeImmediately) contextAction.executeCallback(null, null); - else this.setContextAction(contextAction); - } - }, keyUpCallback: () => { - if (getApp().getState() === OlympusState.UNIT_CONTROL && !contextAction.getOptions().executeImmediately) { + const contextActionSet = this.getContextActionSet(); + if (this.getContextAction() === null || contextAction !== this.getContextAction()) { + if (getApp().getState() === OlympusState.UNIT_CONTROL && contextActionSet && contextAction.getId() in contextActionSet.getContextActions()) { + if (contextAction.getOptions().executeImmediately) contextAction.executeCallback(null, null); + else this.setContextAction(contextAction); + } + } else { this.setContextAction(null); } }, @@ -381,30 +411,44 @@ export class Map extends L.Map { keyUpCallback: (ev: KeyboardEvent) => (this.#panUp = false), keyDownCallback: (ev: KeyboardEvent) => (this.#panUp = true), code: "KeyW", + shiftKey: false, + altKey: false, + ctrlKey: false, }) .addShortcut(`panDown`, { label: "Pan map down", keyUpCallback: (ev: KeyboardEvent) => (this.#panDown = false), keyDownCallback: (ev: KeyboardEvent) => (this.#panDown = true), code: "KeyS", + shiftKey: false, + altKey: false, + ctrlKey: false, }) .addShortcut(`panLeft`, { label: "Pan map left", keyUpCallback: (ev: KeyboardEvent) => (this.#panLeft = false), keyDownCallback: (ev: KeyboardEvent) => (this.#panLeft = true), code: "KeyA", + shiftKey: false, + altKey: false, + ctrlKey: false, }) .addShortcut(`panRight`, { label: "Pan map right", keyUpCallback: (ev: KeyboardEvent) => (this.#panRight = false), keyDownCallback: (ev: KeyboardEvent) => (this.#panRight = true), code: "KeyD", + shiftKey: false, + altKey: false, + ctrlKey: false, }) .addShortcut(`panFast`, { label: "Pan map fast", keyUpCallback: (ev: KeyboardEvent) => (this.#panFast = false), keyDownCallback: (ev: KeyboardEvent) => (this.#panFast = true), code: "ShiftLeft", + altKey: false, + ctrlKey: false, }); /* Periodically check if the camera control endpoint is available */ @@ -580,7 +624,7 @@ export class Map extends L.Map { } isSelecting() { - return this.#selecting; + return this.#isSelecting; } setTheatre(theatre: string) { @@ -670,6 +714,17 @@ export class Map extends L.Map { return this.#previousZoom; } + setKeepRelativePositions(keepRelativePositions: boolean) { + this.#keepRelativePositions = keepRelativePositions; + this.#updateDestinationPreviewMarkers(); + if (keepRelativePositions) this.scrollWheelZoom.disable(); + else this.scrollWheelZoom.enable(); + } + + getKeepRelativePositions() { + return this.#keepRelativePositions; + } + increaseCameraZoom() { //const slider = document.querySelector(`label[title="${DCS_LINK_RATIO}"] input`); //if (slider instanceof HTMLInputElement) { @@ -762,166 +817,217 @@ export class Map extends L.Map { } #onSelectionStart(e: any) { - this.#selecting = true; + this.#isSelecting = true; } #onSelectionEnd(e: any) { getApp().getUnitsManager().selectFromBounds(e.selectionBounds); - this.#selecting = false; + + /* Delay the event so that any other event in the queue still sees the map in selection mode */ + window.setTimeout(() => { + this.#isSelecting = false; + }, 300); } #onMouseUp(e: any) { - this.#isMouseDown = false; - this.#isRotatingDestination = false; + if (e.originalEvent?.button === 0) { + if (Date.now() - this.#leftMouseDownEpoch < SHORT_PRESS_MILLISECONDS) this.#onLeftShortClick(e); + this.#isLeftMouseDown = false; + } else if (e.originalEvent?.button === 2) { + if (Date.now() - this.#rightMouseDownEpoch < SHORT_PRESS_MILLISECONDS) this.#onRightShortClick(e); + this.#isRightMouseDown = false; + } } #onMouseDown(e: any) { - this.#isMouseDown = true; - if (this.#contextAction?.getTarget() === ContextActionTarget.POINT && e.originalEvent?.button === 2) this.#isRotatingDestination = true; + if (e.originalEvent?.button === 0) { + this.#isLeftMouseDown = true; + this.#leftMouseDownEpoch = Date.now(); + } else if (e.originalEvent?.button === 2) { + this.#isRightMouseDown = true; + this.#rightMouseDownEpoch = Date.now(); + this.#rightMouseDownTimeout = window.setTimeout(() => { + this.#onRightLongClick(e); + }, SHORT_PRESS_MILLISECONDS); + } } - #onLeftClick(e: L.LeafletMouseEvent) { - console.log(`Left click at ${e.latlng}`); + #onMouseWheel(e: any) { + if (this.#keepRelativePositions) { + this.#destinationRotation += e.deltaY / 20; + this.#moveDestinationPreviewMarkers(); + } + } - /* Execute the short click action */ - if (getApp().getState() === OlympusState.IDLE) { - /* Do nothing */ - } else if (getApp().getState() === OlympusState.SPAWN_CONTEXT) { - getApp().setState(OlympusState.IDLE); - } else if (getApp().getState() === OlympusState.SPAWN) { - if (getApp().getSubState() === SpawnSubState.SPAWN_UNIT) { - if (this.#spawnRequestTable !== null) { - this.#spawnRequestTable.unit.location = e.latlng; - getApp() - .getUnitsManager() - .spawnUnits( - this.#spawnRequestTable.category, - Array(this.#spawnRequestTable.amount).fill(this.#spawnRequestTable.unit), - this.#spawnRequestTable.coalition, - false, - undefined, - undefined, - (hash) => { - this.addTemporaryMarker(e.latlng, this.#spawnRequestTable?.unit.unitType ?? "unknown", this.#spawnRequestTable?.coalition ?? "blue", hash); + #onLeftShortClick(e: L.LeafletMouseEvent) { + if (this.#debounceTimeout) window.clearTimeout(this.#debounceTimeout); + + if (Date.now() - this.#leftMouseDownEpoch < SHORT_PRESS_MILLISECONDS) { + this.#debounceTimeout = window.setTimeout(() => { + if (!this.#isSelecting) { + console.log(`Left short click at ${e.latlng}`); + + /* Execute the short click action */ + if (getApp().getState() === OlympusState.IDLE) { + /* Do nothing */ + } else if (getApp().getState() === OlympusState.SPAWN) { + if (getApp().getSubState() === SpawnSubState.SPAWN_UNIT) { + if (this.#spawnRequestTable !== null) { + this.#spawnRequestTable.unit.location = e.latlng; + getApp() + .getUnitsManager() + .spawnUnits( + this.#spawnRequestTable.category, + Array(this.#spawnRequestTable.amount).fill(this.#spawnRequestTable.unit), + this.#spawnRequestTable.coalition, + false, + undefined, + undefined, + (hash) => { + this.addTemporaryMarker( + e.latlng, + this.#spawnRequestTable?.unit.unitType ?? "unknown", + this.#spawnRequestTable?.coalition ?? "blue", + hash + ); + } + ); } - ); - } - } else if (getApp().getSubState() === SpawnSubState.SPAWN_EFFECT) { - if (this.#effectRequestTable !== null) { - if (this.#effectRequestTable.type === "explosion") { - if (this.#effectRequestTable.explosionType === "High explosive") getApp().getServerManager().spawnExplosion(50, "normal", e.latlng); - else if (this.#effectRequestTable.explosionType === "Napalm") getApp().getServerManager().spawnExplosion(50, "napalm", e.latlng); - else if (this.#effectRequestTable.explosionType === "White phosphorous") getApp().getServerManager().spawnExplosion(50, "phosphorous", e.latlng); + } else if (getApp().getSubState() === SpawnSubState.SPAWN_EFFECT) { + if (this.#effectRequestTable !== null) { + if (this.#effectRequestTable.type === "explosion") { + if (this.#effectRequestTable.explosionType === "High explosive") getApp().getServerManager().spawnExplosion(50, "normal", e.latlng); + else if (this.#effectRequestTable.explosionType === "Napalm") getApp().getServerManager().spawnExplosion(50, "napalm", e.latlng); + else if (this.#effectRequestTable.explosionType === "White phosphorous") + getApp().getServerManager().spawnExplosion(50, "phosphorous", e.latlng); - this.addExplosionMarker(e.latlng); - } else if (this.#effectRequestTable.type === "smoke") { - getApp() - .getServerManager() - .spawnSmoke(this.#effectRequestTable.smokeColor ?? "white", e.latlng); - this.addSmokeMarker(e.latlng, this.#effectRequestTable.smokeColor ?? "white"); + this.addExplosionMarker(e.latlng); + } else if (this.#effectRequestTable.type === "smoke") { + getApp() + .getServerManager() + .spawnSmoke(this.#effectRequestTable.smokeColor ?? "white", e.latlng); + this.addSmokeMarker(e.latlng, this.#effectRequestTable.smokeColor ?? "white"); + } + } + } + } else if (getApp().getState() === OlympusState.DRAW) { + if (getApp().getSubState() === DrawSubState.DRAW_POLYGON) { + const selectedArea = this.getSelectedCoalitionArea(); + if (selectedArea && selectedArea instanceof CoalitionPolygon) { + selectedArea.addTemporaryLatLng(e.latlng); + } + } else if (getApp().getSubState() === DrawSubState.DRAW_CIRCLE) { + const selectedArea = this.getSelectedCoalitionArea(); + if (selectedArea && selectedArea instanceof CoalitionCircle) { + if (selectedArea.getLatLng().lat == 0 && selectedArea.getLatLng().lng == 0) selectedArea.setLatLng(e.latlng); + getApp().setState(OlympusState.DRAW, DrawSubState.EDIT); + } + } else if (getApp().getSubState() == DrawSubState.NO_SUBSTATE) { + this.deselectAllCoalitionAreas(); + for (let idx = 0; idx < this.#coalitionAreas.length; idx++) { + if (areaContains(e.latlng, this.#coalitionAreas[idx])) { + this.#coalitionAreas[idx].setSelected(true); + getApp().setState(OlympusState.DRAW, DrawSubState.EDIT); + break; + } + } + } + } else if (getApp().getState() === OlympusState.JTAC) { + // TODO less redundant way to do this + if (getApp().getSubState() === JTACSubState.SELECT_TARGET) { + if (!this.#targetPoint) { + this.#targetPoint = new TextMarker(e.latlng, "BP", "rgb(37 99 235)", { interactive: true, draggable: true }); + this.#targetPoint.addTo(this); + this.#targetPoint.on("dragstart", (event) => { + event.target.options["freeze"] = true; + }); + this.#targetPoint.on("dragend", (event) => { + getApp().setState(OlympusState.JTAC); + event.target.options["freeze"] = false; + }); + this.#targetPoint.on("click", (event) => { + getApp().setState(OlympusState.JTAC); + }); + } else this.#targetPoint.setLatLng(e.latlng); + } else if (getApp().getSubState() === JTACSubState.SELECT_ECHO_POINT) { + if (!this.#ECHOPoint) { + this.#ECHOPoint = new TextMarker(e.latlng, "BP", "rgb(37 99 235)", { interactive: true, draggable: true }); + this.#ECHOPoint.addTo(this); + this.#ECHOPoint.on("dragstart", (event) => { + event.target.options["freeze"] = true; + }); + this.#ECHOPoint.on("dragend", (event) => { + getApp().setState(OlympusState.JTAC); + event.target.options["freeze"] = false; + }); + this.#ECHOPoint.on("click", (event) => { + getApp().setState(OlympusState.JTAC); + }); + } else this.#ECHOPoint.setLatLng(e.latlng); + } else if (getApp().getSubState() === JTACSubState.SELECT_IP) { + if (!this.#IPPoint) { + this.#IPPoint = new TextMarker(e.latlng, "BP", "rgb(37 99 235)", { interactive: true, draggable: true }); + this.#IPPoint.addTo(this); + this.#IPPoint.on("dragstart", (event) => { + event.target.options["freeze"] = true; + }); + this.#IPPoint.on("dragend", (event) => { + getApp().setState(OlympusState.JTAC); + event.target.options["freeze"] = false; + }); + this.#IPPoint.on("click", (event) => { + getApp().setState(OlympusState.JTAC); + }); + } else this.#IPPoint.setLatLng(e.latlng); + } + getApp().setState(OlympusState.JTAC); + this.#drawIPToTargetLine(); + } else { + if (getApp().getSubState() === NO_SUBSTATE) getApp().setState(OlympusState.IDLE); + else getApp().setState(OlympusState.UNIT_CONTROL); } } - } - } else if (getApp().getState() === OlympusState.DRAW) { - if (getApp().getSubState() === DrawSubState.DRAW_POLYGON) { - const selectedArea = this.getSelectedCoalitionArea(); - if (selectedArea && selectedArea instanceof CoalitionPolygon) { - selectedArea.addTemporaryLatLng(e.latlng); - } - } else if (getApp().getSubState() === DrawSubState.DRAW_CIRCLE) { - const selectedArea = this.getSelectedCoalitionArea(); - if (selectedArea && selectedArea instanceof CoalitionCircle) { - if (selectedArea.getLatLng().lat == 0 && selectedArea.getLatLng().lng == 0) selectedArea.setLatLng(e.latlng); - getApp().setState(OlympusState.DRAW, DrawSubState.EDIT); - } - } else if (getApp().getSubState() == DrawSubState.NO_SUBSTATE) { - this.deselectAllCoalitionAreas(); - for (let idx = 0; idx < this.#coalitionAreas.length; idx++) { - if (areaContains(e.latlng, this.#coalitionAreas[idx])) { - this.#coalitionAreas[idx].setSelected(true); - getApp().setState(OlympusState.DRAW, DrawSubState.EDIT); - break; - } - } - } + }, DEBOUNCE_MILLISECONDS); + } + } + + #onRightShortClick(e: L.LeafletMouseEvent) { + console.log(`Right short click at ${e.latlng}`); + + window.clearTimeout(this.#rightMouseDownTimeout); + + if (getApp().getState() === OlympusState.IDLE || getApp().getState() === OlympusState.SPAWN_CONTEXT) { + SpawnContextMenuRequestEvent.dispatch(e.latlng); + getApp().setState(OlympusState.SPAWN_CONTEXT); } else if (getApp().getState() === OlympusState.UNIT_CONTROL) { if (this.#contextAction !== null) this.executeContextAction(null, e.latlng, e.originalEvent); - else getApp().setState(OlympusState.IDLE); - } else if (getApp().getState() === OlympusState.JTAC) { - // TODO less redundant way to do this - if (getApp().getSubState() === JTACSubState.SELECT_TARGET) { - if (!this.#targetPoint) { - this.#targetPoint = new TextMarker(e.latlng, "BP", "rgb(37 99 235)", { interactive: true, draggable: true }); - this.#targetPoint.addTo(this); - this.#targetPoint.on("dragstart", (event) => { - event.target.options["freeze"] = true; - }); - this.#targetPoint.on("dragend", (event) => { - getApp().setState(OlympusState.JTAC); - event.target.options["freeze"] = false; - }); - this.#targetPoint.on("click", (event) => { - getApp().setState(OlympusState.JTAC); - }); - } else this.#targetPoint.setLatLng(e.latlng); - } else if (getApp().getSubState() === JTACSubState.SELECT_ECHO_POINT) { - if (!this.#ECHOPoint) { - this.#ECHOPoint = new TextMarker(e.latlng, "BP", "rgb(37 99 235)", { interactive: true, draggable: true }); - this.#ECHOPoint.addTo(this); - this.#ECHOPoint.on("dragstart", (event) => { - event.target.options["freeze"] = true; - }); - this.#ECHOPoint.on("dragend", (event) => { - getApp().setState(OlympusState.JTAC); - event.target.options["freeze"] = false; - }); - this.#ECHOPoint.on("click", (event) => { - getApp().setState(OlympusState.JTAC); - }); - } else this.#ECHOPoint.setLatLng(e.latlng); - } else if (getApp().getSubState() === JTACSubState.SELECT_IP) { - if (!this.#IPPoint) { - this.#IPPoint = new TextMarker(e.latlng, "BP", "rgb(37 99 235)", { interactive: true, draggable: true }); - this.#IPPoint.addTo(this); - this.#IPPoint.on("dragstart", (event) => { - event.target.options["freeze"] = true; - }); - this.#IPPoint.on("dragend", (event) => { - getApp().setState(OlympusState.JTAC); - event.target.options["freeze"] = false; - }); - this.#IPPoint.on("click", (event) => { - getApp().setState(OlympusState.JTAC); - }); - } else this.#IPPoint.setLatLng(e.latlng); - } - getApp().setState(OlympusState.JTAC); - this.#drawIPToTargetLine(); - } else { + else this.executeDefaultContextAction(null, e.latlng, e.originalEvent); } } - #onRightClick(e: L.LeafletMouseEvent) { - e.originalEvent.preventDefault(); + #onRightLongClick(e: L.LeafletMouseEvent) { + console.log(`Right long click at ${e.latlng}`); - console.log(`Right click at ${e.latlng}`); - - if (!this.#isDragging && !this.#isZooming) { - this.deselectAllCoalitionAreas(); - if (getApp().getState() === OlympusState.IDLE || getApp().getState() === OlympusState.SPAWN_CONTEXT) { - SpawnContextMenuRequestEvent.dispatch(e.latlng); - getApp().setState(OlympusState.SPAWN_CONTEXT); - } else if (getApp().getState() === OlympusState.UNIT_CONTROL) { - if (!this.getContextAction()) { - getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.MAP_CONTEXT_MENU); - MapContextMenuRequestEvent.dispatch(e.latlng); - } + if (getApp().getState() === OlympusState.UNIT_CONTROL) { + if (!this.getContextAction()) { + getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.MAP_CONTEXT_MENU); + MapContextMenuRequestEvent.dispatch(e.latlng); } } } + #onDoubleClick(e: L.LeafletMouseEvent) { + console.log(`Double click at ${e.latlng}`); + + if (this.#debounceTimeout) window.clearTimeout(this.#debounceTimeout); + + if (getApp().getSubState() === NO_SUBSTATE) getApp().setState(OlympusState.IDLE); + else getApp().setState(getApp().getState()); + } + #onMouseMove(e: any) { if (!this.#isRotatingDestination) { + this.#destionationWasRotated = false; this.#lastMousePosition.x = e.originalEvent.x; this.#lastMousePosition.y = e.originalEvent.y; this.#lastMouseCoordinates = e.latlng; @@ -934,6 +1040,7 @@ export class Map extends L.Map { if (this.#currentSpawnMarker) this.#currentSpawnMarker.setLatLng(e.latlng); if (this.#currentEffectMarker) this.#currentEffectMarker.setLatLng(e.latlng); } else { + this.#destionationWasRotated = true; this.#destinationRotation -= e.originalEvent.movementX; } @@ -1047,18 +1154,20 @@ export class Map extends L.Map { delete this.#destinationPreviewMarkers[ID]; }); - selectedUnits.forEach((unit) => { - if (this.#contextAction?.getOptions().type === ContextActionType.MOVE) { - this.#destinationPreviewMarkers[unit.ID] = new TemporaryUnitMarker(new L.LatLng(0, 0), unit.getName(), unit.getCoalition()); - } else if (this.#contextAction?.getTarget() === ContextActionTarget.POINT) { - this.#destinationPreviewMarkers[unit.ID] = new TargetMarker(new L.LatLng(0, 0)); - } - this.#destinationPreviewMarkers[unit.ID]?.addTo(this); - }); + if (this.#keepRelativePositions) { + selectedUnits.forEach((unit) => { + if (this.#contextAction?.getOptions().type === ContextActionType.MOVE || this.#contextAction === null) { + this.#destinationPreviewMarkers[unit.ID] = new TemporaryUnitMarker(new L.LatLng(0, 0), unit.getName(), unit.getCoalition()); + } else if (this.#contextAction?.getTarget() === ContextActionTarget.POINT) { + this.#destinationPreviewMarkers[unit.ID] = new TargetMarker(new L.LatLng(0, 0)); + } + this.#destinationPreviewMarkers[unit.ID]?.addTo(this); + }); + } } #moveDestinationPreviewMarkers() { - if (this.#options.keepRelativePositions) { + if (this.#keepRelativePositions) { Object.entries(getApp().getUnitsManager().computeGroupDestination(this.#lastMouseCoordinates, this.#destinationRotation)).forEach(([ID, latlng]) => { this.#destinationPreviewMarkers[ID]?.setLatLng(latlng); }); diff --git a/frontend/react/src/olympusapp.ts b/frontend/react/src/olympusapp.ts index e783d61f..f61fac9d 100644 --- a/frontend/react/src/olympusapp.ts +++ b/frontend/react/src/olympusapp.ts @@ -160,7 +160,6 @@ export class OlympusApp { }); this.#shortcutManager.checkShortcuts(); - } getConfig() { @@ -189,13 +188,55 @@ export class OlympusApp { console.log(`Profile ${this.#profileName} saved correctly`); } else { this.addInfoMessage("Error saving profile"); - throw new Error("Error saving profile file"); + throw new Error("Error saving profile"); } }) // Parse the response as JSON .catch((error) => console.error(error)); // Handle errors } } + resetProfile() { + if (this.#profileName !== null) { + const requestOptions = { + method: "PUT", // Specify the request method + headers: { "Content-Type": "application/json" }, // Specify the content type + body: "", // Send the data in JSON format + }; + + fetch(this.getExpressAddress() + `/resources/profile/reset/${this.#profileName}`, requestOptions) + .then((response) => { + if (response.status === 200) { + console.log(`Profile ${this.#profileName} reset correctly`); + location.reload() + } else { + this.addInfoMessage("Error resetting profile"); + throw new Error("Error resetting profile"); + } + }) // Parse the response as JSON + .catch((error) => console.error(error)); // Handle errors + } + } + + resetAllProfiles() { + const requestOptions = { + method: "PUT", // Specify the request method + headers: { "Content-Type": "application/json" }, // Specify the content type + body: "", // Send the data in JSON format + }; + + fetch(this.getExpressAddress() + `/resources/profile/resetall`, requestOptions) + .then((response) => { + if (response.status === 200) { + console.log(`All profiles reset correctly`); + location.reload() + } else { + this.addInfoMessage("Error resetting profiles"); + throw new Error("Error resetting profiles"); + } + }) // 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; @@ -208,9 +249,9 @@ export class OlympusApp { this.#map?.setOptions(profile.mapOptions); this.#shortcutManager?.setShortcutsOptions(profile.shortcuts); this.addInfoMessage("Profile loaded correctly"); - console.log(`Profile ${this.#profileName} saved correctly`); + console.log(`Profile ${this.#profileName} loaded correctly`); } else { - this.addInfoMessage("Error loading profile"); + this.addInfoMessage("Profile not found, creating new profile"); console.log(`Error loading profile`); } } diff --git a/frontend/react/src/shortcut/shortcut.ts b/frontend/react/src/shortcut/shortcut.ts index 0da40a38..406b097b 100644 --- a/frontend/react/src/shortcut/shortcut.ts +++ b/frontend/react/src/shortcut/shortcut.ts @@ -11,33 +11,31 @@ export class Shortcut { this.#id = id; this.#options = options; - AppStateChangedEvent.on(() => this.#keydown = false) - - /* Key up event is mandatory */ + AppStateChangedEvent.on(() => (this.#keydown = false)); + + /* On keyup, it is enough to check the code only, not the entire combination */ document.addEventListener("keyup", (ev: any) => { - this.#keydown = false; - if (keyEventWasInInput(ev) || options.code !== ev.code) return; - if ( - ev.altKey === (options.altKey ?? ev.code.indexOf("Alt") >= 0) && - ev.ctrlKey === (options.ctrlKey ?? ev.code.indexOf("Ctrl") >= 0) && - ev.shiftKey === (options.shiftKey ?? ev.code.indexOf("Shift") >= 0) - ) + if (this.#keydown && options.code === ev.code) { + ev.preventDefault(); options.keyUpCallback(ev); + this.#keydown = false; + } }); - /* Key down event is optional */ - if (options.keyDownCallback) { - document.addEventListener("keydown", (ev: any) => { - if (this.#keydown || keyEventWasInInput(ev) || options.code !== ev.code) return; + /* On keydown, check exactly if the requested key combination is being pressed */ + document.addEventListener("keydown", (ev: any) => { + if ( + !(this.#keydown || keyEventWasInInput(ev) || options.code !== ev.code) && + (options.altKey === undefined || ev.altKey === (options.altKey ?? ev.code.indexOf("Alt") >= 0)) && + (options.ctrlKey === undefined || ev.ctrlKey === (options.ctrlKey ?? ev.code.indexOf("Control") >= 0)) && + (options.shiftKey === undefined || ev.shiftKey === (options.shiftKey ?? ev.code.indexOf("Shift") >= 0)) + ) { + ev.preventDefault(); this.#keydown = true; - if ( - ev.altKey === (options.altKey ?? ev.code.indexOf("Alt") >= 0) && - ev.ctrlKey === (options.ctrlKey ?? ev.code.indexOf("Control") >= 0) && - ev.shiftKey === (options.shiftKey ?? ev.code.indexOf("Shift") >= 0) - ) - if (options.keyDownCallback) options.keyDownCallback(ev); - }); - } + + if (options.keyDownCallback) options.keyDownCallback(ev); /* Key down event is optional */ + } + }); } getOptions() { @@ -51,4 +49,19 @@ export class Shortcut { getId() { return this.#id; } + + toActions() { + let actions: string[] = []; + if (this.getOptions().shiftKey) actions.push("Shift"); + if (this.getOptions().altKey) actions.push("Alt"); + if (this.getOptions().ctrlKey) actions.push("Ctrl") + actions.push(this.getOptions().code.replace("Key", "") + .replace("ControlLeft", "Left Ctrl") + .replace("AltLeft", "Left Alt") + .replace("ShiftLeft", "Left Shift") + .replace("ControlRight", "Right Ctrl") + .replace("AltRight", "Right Alt") + .replace("ShiftRight", "Right Shift")) + return actions + } } diff --git a/frontend/react/src/shortcut/shortcutmanager.ts b/frontend/react/src/shortcut/shortcutmanager.ts index 05807800..e5ad2af2 100644 --- a/frontend/react/src/shortcut/shortcutmanager.ts +++ b/frontend/react/src/shortcut/shortcutmanager.ts @@ -5,14 +5,7 @@ import { Shortcut } from "./shortcut"; export class ShortcutManager { #shortcuts: { [key: string]: Shortcut } = {}; - constructor() { - // Stop ctrl+digits from sending the browser to another tab - document.addEventListener("keydown", (ev: KeyboardEvent) => { - if (ev.code.indexOf("Digit") >= 0 && ev.ctrlKey === true && ev.altKey === false && ev.shiftKey === false) { - ev.preventDefault(); - } - }); - } + constructor() {} addShortcut(id: string, shortcutOptions: ShortcutOptions) { this.#shortcuts[id] = new Shortcut(id, shortcutOptions); @@ -20,6 +13,14 @@ export class ShortcutManager { return this; } + getShortcut(id) { + return this.#shortcuts[id]; + } + + getShortcuts() { + return this.#shortcuts; + } + getShortcutsOptions() { let shortcutsOptions = {}; for (let id in this.#shortcuts) { @@ -48,11 +49,18 @@ export class ShortcutManager { const otherShortcut = this.#shortcuts[otherid]; if (shortcut.getOptions().code === otherShortcut.getOptions().code) { if ( - (shortcut.getOptions().altKey ?? false) === (otherShortcut.getOptions().altKey ?? false) && - (shortcut.getOptions().ctrlKey ?? false) === (otherShortcut.getOptions().ctrlKey ?? false) && - (shortcut.getOptions().shiftKey ?? false) === (otherShortcut.getOptions().shiftKey ?? false) + shortcut.getOptions().code === otherShortcut.getOptions().code && + ((shortcut.getOptions().shiftKey === undefined && otherShortcut.getOptions().shiftKey !== undefined) || + (shortcut.getOptions().shiftKey !== undefined && otherShortcut.getOptions().shiftKey === undefined) || + shortcut.getOptions().shiftKey === otherShortcut.getOptions().shiftKey) && + ((shortcut.getOptions().altKey === undefined && otherShortcut.getOptions().altKey !== undefined) || + (shortcut.getOptions().altKey !== undefined && otherShortcut.getOptions().altKey === undefined) || + shortcut.getOptions().altKey === otherShortcut.getOptions().altKey) && + ((shortcut.getOptions().ctrlKey === undefined && otherShortcut.getOptions().ctrlKey !== undefined) || + (shortcut.getOptions().ctrlKey !== undefined && otherShortcut.getOptions().ctrlKey === undefined) || + shortcut.getOptions().ctrlKey === otherShortcut.getOptions().ctrlKey) ) { - console.error("Duplicate shortcut: " + shortcut.getOptions().label + " and " + otherShortcut.getOptions().label) + console.error("Duplicate shortcut: " + shortcut.getOptions().label + " and " + otherShortcut.getOptions().label); } } } diff --git a/frontend/react/src/types/types.ts b/frontend/react/src/types/types.ts index cd55151d..522c6235 100644 --- a/frontend/react/src/types/types.ts +++ b/frontend/react/src/types/types.ts @@ -21,7 +21,6 @@ export type MapOptions = { fillSelectedRing: boolean; showMinimap: boolean; protectDCSUnits: boolean; - keepRelativePositions: boolean; cameraPluginPort: number; cameraPluginRatio: number; cameraPluginEnabled: boolean; diff --git a/frontend/react/src/ui/components/olstatebutton.tsx b/frontend/react/src/ui/components/olstatebutton.tsx index c1095eb3..1ee59e5b 100644 --- a/frontend/react/src/ui/components/olstatebutton.tsx +++ b/frontend/react/src/ui/components/olstatebutton.tsx @@ -11,6 +11,8 @@ export function OlStateButton(props: { icon?: IconProp; tooltip: string; onClick: () => void; + onMouseUp?: () => void; + onMouseDown?: () => void; children?: JSX.Element | JSX.Element[]; }) { const [hover, setHover] = useState(false); @@ -36,8 +38,10 @@ export function OlStateButton(props: { ref={buttonRef} onClick={() => { props.onClick(); - setHover(false); + props.onClick ?? setHover(false); }} + onMouseUp={props.onMouseUp ?? (() => {})} + onMouseDown={props.onMouseDown ?? (() => {})} data-checked={props.checked} type="button" className={className} diff --git a/frontend/react/src/ui/components/oltoggle.tsx b/frontend/react/src/ui/components/oltoggle.tsx index 0f005213..50969688 100644 --- a/frontend/react/src/ui/components/oltoggle.tsx +++ b/frontend/react/src/ui/components/oltoggle.tsx @@ -5,8 +5,7 @@ export function OlToggle(props: { toggled: boolean | undefined; onClick: () => v
- The profile name you choose determines what keybinds/groups/options get loaded and edited. Be careful! + The profile name you choose determines the saved key binds, groups and options you see.
); diff --git a/frontend/react/src/ui/panels/optionsmenu.tsx b/frontend/react/src/ui/panels/optionsmenu.tsx index 30c8f9e2..c7546109 100644 --- a/frontend/react/src/ui/panels/optionsmenu.tsx +++ b/frontend/react/src/ui/panels/optionsmenu.tsx @@ -9,6 +9,7 @@ import { BindShortcutRequestEvent, MapOptionsChangedEvent, ShortcutsChangedEvent import { OlAccordion } from "../components/olaccordion"; import { Shortcut } from "../../shortcut/shortcut"; import { OlSearchBar } from "../components/olsearchbar"; +import { FaTrash, FaXmark } from "react-icons/fa6"; const enum Accordion { NONE, @@ -32,14 +33,12 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
- setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.BINDINGS: Accordion.NONE ) - } + onClick={() => setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.BINDINGS : Accordion.NONE)} open={openAccordion === Accordion.BINDINGS} title="Key bindings" > @@ -65,10 +64,40 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre }} > {shortcut.getOptions().label} - - {shortcut.getOptions().altKey ? "Alt + " : ""} - {shortcut.getOptions().ctrlKey ? "Ctrl + " : ""} - {shortcut.getOptions().shiftKey ? "Shift + " : ""} + + {shortcut.getOptions().altKey ? ( +
+
Alt
+{" "} +
+ ) : shortcut.getOptions().altKey === false ? ( +
+
Alt
+{" "} +
+ ) : ( + "" + )} + {shortcut.getOptions().ctrlKey ? ( +
+
Shift
+{" "} +
+ ) : shortcut.getOptions().ctrlKey === false ? ( +
+
Shift
+{" "} +
+ ) : ( + "" + )} + {shortcut.getOptions().shiftKey ? ( +
+
Ctrl
+{" "} +
+ ) : shortcut.getOptions().shiftKey === false ? ( +
+
Ctrl
+{" "} +
+ ) : ( + "" + )} {shortcut.getOptions().code}
@@ -77,7 +106,11 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre - setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.MAP_OPTIONS: Accordion.NONE )} open={openAccordion === Accordion.MAP_OPTIONS} title="Map options"> + setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.MAP_OPTIONS : Accordion.NONE)} + open={openAccordion === Accordion.MAP_OPTIONS} + title="Map options" + >
void; childre {}}> Hide Short range Rings
-
getApp().getMap().setOption("keepRelativePositions", !mapOptions.keepRelativePositions)} - > - {}}> - Keep units relative positions -
+
void; childre
- setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.CAMERA_PLUGIN: Accordion.NONE )} open={openAccordion === Accordion.CAMERA_PLUGIN} title="Camera plugin options"> + setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.CAMERA_PLUGIN : Accordion.NONE)} + open={openAccordion === Accordion.CAMERA_PLUGIN} + title="Camera plugin options" + >
void; childre
+ +
+ + +
); diff --git a/frontend/react/src/ui/panels/radiossummarypanel.tsx b/frontend/react/src/ui/panels/radiossummarypanel.tsx new file mode 100644 index 00000000..fbb564a5 --- /dev/null +++ b/frontend/react/src/ui/panels/radiossummarypanel.tsx @@ -0,0 +1,74 @@ +import React, { useEffect, useState } from "react"; +import { AudioSinksChangedEvent } from "../../events"; +import { AudioSink } from "../../audio/audiosink"; +import { RadioSink } from "../../audio/radiosink"; +import { FaJetFighter, FaRadio } from "react-icons/fa6"; +import { OlStateButton } from "../components/olstatebutton"; +import { UnitSink } from "../../audio/unitsink"; + +export function RadiosSummaryPanel(props: {}) { + const [audioSinks, setAudioSinks] = useState([] as AudioSink[]); + + useEffect(() => { + AudioSinksChangedEvent.on((audioSinks) => setAudioSinks(audioSinks)); + }, []); + + return ( + <> + {audioSinks.length > 0 && ( +
+
+ + {audioSinks + .filter((audioSinks) => audioSinks instanceof RadioSink) + .map((radioSink, idx) => { + return ( + {}} + onMouseDown={() => { + radioSink.setPtt(true); + }} + onMouseUp={() => { + radioSink.setPtt(false); + }} + tooltip="Click to talk, lights up when receiving" + > + {idx + 1} + + ); + })} + + + {audioSinks + .filter((audioSinks) => audioSinks instanceof UnitSink) + .map((radioSink, idx) => { + return ( + {}} + onMouseDown={() => { + radioSink.setPtt(true); + }} + onMouseUp={() => { + radioSink.setPtt(false); + }} + tooltip="Click to talk" + > + {idx + 1} + + ); + })} +
+
+ )} + + ); +} diff --git a/frontend/react/src/ui/panels/unitcontrolbar.tsx b/frontend/react/src/ui/panels/unitcontrolbar.tsx index 8c203e93..edbb374c 100644 --- a/frontend/react/src/ui/panels/unitcontrolbar.tsx +++ b/frontend/react/src/ui/panels/unitcontrolbar.tsx @@ -8,6 +8,7 @@ import { FaInfoCircle } from "react-icons/fa"; import { FaChevronDown, FaChevronLeft, FaChevronRight, FaChevronUp } from "react-icons/fa6"; import { OlympusState } from "../../constants/constants"; import { AppStateChangedEvent, ContextActionChangedEvent, ContextActionSetChangedEvent, MapOptionsChangedEvent } from "../../events"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; export function UnitControlBar(props: {}) { const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED); @@ -111,24 +112,20 @@ export function UnitControlBar(props: {}) { {contextAction && (
-