More work on unit context actions

This commit is contained in:
Davide Passoni
2024-06-11 16:19:29 +02:00
parent b089a8cfd8
commit 79016d72e3
12 changed files with 258 additions and 82 deletions

View File

@@ -49,7 +49,7 @@ export function OlDropdown(props: {
if (cxr > window.innerWidth)
offsetX -= (cxr - window.innerWidth)
if (cyb > window.innerHeight)
offsetY = -ch - 8;
offsetY -= bh + ch + 16;
/* Apply the offset */
content.style.left = `${offsetX}px`

View File

@@ -1,42 +1,61 @@
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { faExternalLink, faLock, faLockOpen, faUnlock, faUnlockAlt } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import React from "react"
import React, { useRef, useState } from "react"
import { OlTooltip } from "./oltooltip";
export function OlStateButton(props: {
className?: string,
checked: boolean,
icon: IconProp,
tooltip: string,
onClick: () => void
}) {
var [hover, setHover] = useState(false);
var buttonRef = useRef(null);
const className = (props.className ?? '') + ` h-[40px] w-[40px] flex-none font-medium rounded-md text-lg dark:bg-olympus-600 dark:hover:bg-olympus-300 dark:data-[checked='true']:bg-blue-500 dark:data-[checked='true']:text-white dark:text-gray-300 dark:border-gray-600 `;
return <button onClick={props.onClick} data-checked={props.checked} type="button" className={className}>
return <><button ref={buttonRef} onClick={props.onClick} data-checked={props.checked} type="button" className={className} onMouseEnter={() => { setHover(true) }} onMouseLeave={() => { setHover(false) }}>
<FontAwesomeIcon icon={props.icon} />
</button>
{hover && <OlTooltip buttonRef={buttonRef} content={props.tooltip} />}
</>
}
export function OlRoundStateButton(props: {
className?: string,
checked: boolean,
icon: IconProp,
tooltip: string,
onClick: () => void
}) {
var [hover, setHover] = useState(false);
var buttonRef = useRef(null);
const className = (props.className ?? '') + ` h-8 w-8 flex-none m-auto border-2 border-gray-900 font-medium rounded-full text-sm dark:bg-[transparent] dark:data-[checked='true']:bg-white dark:text-gray-400 dark:data-[checked='true']:text-gray-900 dark:data-[checked='true']:border-white dark:border-gray-400 dark:data-[checked='true']:hover:bg-gray-200 dark:data-[checked='true']:hover:border-gray-200 dark:hover:bg-gray-800`;
return <button onClick={props.onClick} data-checked={props.checked} type="button" className={className}>
return <><button ref={buttonRef} onClick={props.onClick} data-checked={props.checked} type="button" className={className} onMouseEnter={() => { setHover(true) }} onMouseLeave={() => { setHover(false) }}>
<FontAwesomeIcon className="pt-[3px]" icon={props.icon} />
</button>
{ hover && <OlTooltip buttonRef={buttonRef} content={props.tooltip} /> }
</>
}
export function OlLockStateButton(props: {
className?: string,
checked: boolean,
tooltip: string,
onClick: () => void
}) {
var [hover, setHover] = useState(false);
var buttonRef = useRef(null);
const className = (props.className ?? '') + ` h-8 w-8 flex-none m-auto border-gray-900 font-medium rounded-full text-sm dark:bg-red-500 dark:data-[checked='true']:bg-green-500 dark:text-olympus-900 dark:data-[checked='true']:text-green-900 dark:data-[checked='true']:hover:bg-green-400 dark:hover:bg-red-400`;
return <button onClick={props.onClick} data-checked={props.checked} type="button" className={className}>
<FontAwesomeIcon className="pt-[3px]" icon={props.checked==true ? faUnlockAlt:faLock} />
return <><button ref={buttonRef} onClick={props.onClick} data-checked={props.checked} type="button" className={className} onMouseEnter={() => { setHover(true) }} onMouseLeave={() => { setHover(false) }}>
<FontAwesomeIcon className="pt-[3px]" icon={props.checked == true ? faUnlockAlt : faLock} />
</button>
{ hover && <OlTooltip buttonRef={buttonRef} content={props.tooltip} /> }
</>
}

View File

@@ -0,0 +1,63 @@
import React, { useEffect, useRef, useState } from "react";
export function OlTooltip(props: {
content: string,
buttonRef: React.MutableRefObject<null>
}) {
var contentRef = useRef(null);
function setPosition(content: HTMLDivElement, button: HTMLButtonElement) {
/* Reset the position of the content */
content.style.left = "0px";
content.style.top = "0px";
content.style.height = "";
/* Get the position and size of the button and the content elements */
var [cxl, cyt, cxr, cyb, cw, ch] = [content.getBoundingClientRect().x, content.getBoundingClientRect().y, content.getBoundingClientRect().x + content.clientWidth, content.getBoundingClientRect().y + content.clientHeight, content.clientWidth, content.clientHeight];
var [bxl, byt, bxr, byb, bbw, bh] = [button.getBoundingClientRect().x, button.getBoundingClientRect().y, button.getBoundingClientRect().x + button.clientWidth, button.getBoundingClientRect().y + button.clientHeight, button.clientWidth, button.clientHeight];
/* Limit the maximum height */
if (ch > 400) {
ch = 400;
content.style.height = `${ch}px`;
}
/* Compute the horizontal position of the center of the button and the content */
var cxc = (cxl + cxr) / 2;
var bxc = (bxl + bxr) / 2;
/* Compute the x and y offsets needed to align the button and element horizontally, and to put the content below the button */
var offsetX = bxc - cxc;
var offsetY = byb - cyt + 8;
/* Compute the new position of the left and right margins of the content */
cxl += offsetX;
cxr += offsetX;
cyb += offsetY;
/* Try and move the content so it is inside the screen */
if (cxl < 0)
offsetX -= cxl;
if (cxr > window.innerWidth)
offsetX -= (cxr - window.innerWidth)
if (cyb > window.innerHeight)
offsetY -= bh + ch + 16;
/* Apply the offset */
content.style.left = `${offsetX}px`
content.style.top = `${offsetY}px`
}
useEffect(() => {
if (contentRef.current && props.buttonRef.current) {
const content = contentRef.current as HTMLDivElement;
const button = props.buttonRef.current as HTMLButtonElement;
setPosition(content, button);
}
})
return props.content !== "" && <div ref={contentRef} className={`absolute whitespace-nowrap z-ui-4 px-3 py-2 text-sm font-medium text-white bg-gray-900 rounded-lg shadow-sm dark:bg-gray-700`}>
{ props.content }
</div>
}

View File

@@ -24,7 +24,7 @@ export function Header() {
</div>
</div>
<div className="ml-auto">
<OlLockStateButton checked={false} onClick={() => {}}/>
<OlLockStateButton checked={false} onClick={() => {}} tooltip="Lock/unlock protected units (from scripted mission)"/>
</div>
<div className="flex flex-row h-fit items-center justify-start gap-1">
{
@@ -36,7 +36,8 @@ export function Header() {
getApp().getMap().setHiddenType(entry[0], !appState.mapHiddenTypes[entry[0]]);
}}
checked={!appState.mapHiddenTypes[entry[0]]}
icon={entry[1]} />
icon={entry[1]}
tooltip={"Hide/show " + entry[0] + " units" } />
})
}
</div>
@@ -52,7 +53,8 @@ export function Header() {
getApp().getMap().setHiddenType(entry[0], !appState.mapHiddenTypes[entry[0]]);
}}
checked={!appState.mapHiddenTypes[entry[0]]}
icon={entry[1]} />
icon={entry[1]}
tooltip={"Hide/show " + entry[0] + " units" } />
})
}
</div>
@@ -61,18 +63,21 @@ export function Header() {
<OlRoundStateButton
onClick={() => getApp().getMap().setHiddenType( 'blue', !appState.mapHiddenTypes['blue'] )}
checked={!appState.mapHiddenTypes['blue']}
icon={faFlag} className={"!text-blue-500"} />
icon={faFlag} className={"!text-blue-500"}
tooltip={"Hide/show blue units" } />
<OlRoundStateButton
onClick={() => getApp().getMap().setHiddenType('red', !appState.mapHiddenTypes['red'] )}
checked={!appState.mapHiddenTypes['red']}
icon={faFlag} className={"!text-red-500"} />
icon={faFlag} className={"!text-red-500"}
tooltip={"Hide/show red units" } />
<OlRoundStateButton
onClick={() => getApp().getMap().setHiddenType('neutral', !appState.mapHiddenTypes['neutral'] )}
checked={!appState.mapHiddenTypes['neutral']}
icon={faFlag} className={"!text-gray-500"} />
icon={faFlag} className={"!text-gray-500"}
tooltip={"Hide/show neutral units" } />
</div>
<OlLabelToggle toggled={false} leftLabel={"Live"} rightLabel={"Map"} onClick={() => {}}></OlLabelToggle>
<OlStateButton checked={false} icon={faCamera} onClick={() => {}} />
<OlStateButton checked={false} icon={faCamera} onClick={() => {}} tooltip="Activate/deactivate camera plugin" />
<OlDropdown label="DCS Sat" className="w-40">
<OlDropdownItem className="w-full">DCS Sat</OlDropdownItem>
<OlDropdownItem className="w-full">DCS Alt</OlDropdownItem>

View File

@@ -12,17 +12,17 @@ export function SideBar() {
<nav className="flex flex-col z-ui-1 h-full bg-gray-300 dark:bg-olympus-900">
<div className="flex-1 flex-wrap items-center justify-center p-4 w-16">
<div className="flex flex-col items-center justify-center gap-2.5">
<OlStateButton onClick={events.toggleMainMenuVisible} checked={appState.mainMenuVisible} icon={faEllipsisV}></OlStateButton>
<OlStateButton onClick={events.toggleSpawnMenuVisible} checked={appState.spawnMenuVisible} icon={faPlusSquare}></OlStateButton>
<OlStateButton onClick={events.toggleUnitControlMenuVisible} checked={appState.unitControlMenuVisible} icon={faGamepad}></OlStateButton>
<OlStateButton onClick={events.toggleMeasureMenuVisible} checked={appState.measureMenuVisible} icon={faRuler}></OlStateButton>
<OlStateButton onClick={events.toggleDrawingMenuVisible} checked={appState.drawingMenuVisible} icon={faPencil}></OlStateButton>
<OlStateButton onClick={events.toggleMainMenuVisible} checked={appState.mainMenuVisible} icon={faEllipsisV} tooltip="Hide/show main menu"></OlStateButton>
<OlStateButton onClick={events.toggleSpawnMenuVisible} checked={appState.spawnMenuVisible} icon={faPlusSquare} tooltip="Hide/show unit spawn menu"></OlStateButton>
<OlStateButton onClick={events.toggleUnitControlMenuVisible} checked={appState.unitControlMenuVisible} icon={faGamepad} tooltip=""></OlStateButton>
<OlStateButton onClick={events.toggleMeasureMenuVisible} checked={appState.measureMenuVisible} icon={faRuler} tooltip=""></OlStateButton>
<OlStateButton onClick={events.toggleDrawingMenuVisible} checked={appState.drawingMenuVisible} icon={faPencil} tooltip="Hide/show drawing menu"></OlStateButton>
</div>
</div>
<div className="flex flex-wrap content-end justify-center p-4 w-16">
<div className="flex flex-col items-center justify-center gap-2.5">
<OlStateButton onClick={() => window.open("https://github.com/Pax1601/DCSOlympus/wiki")} checked={false} icon={faQuestionCircle}></OlStateButton>
<OlStateButton onClick={events.toggleOptionsMenuVisible} checked={appState.optionsMenuVisible} icon={faCog}></OlStateButton>
<OlStateButton onClick={() => window.open("https://github.com/Pax1601/DCSOlympus/wiki")} checked={false} icon={faQuestionCircle} tooltip="Open user guide on separate window"></OlStateButton>
<OlStateButton onClick={events.toggleOptionsMenuVisible} checked={appState.optionsMenuVisible} icon={faCog} tooltip="Hide/show settings menu"></OlStateButton>
</div>
</div>
</nav>

View File

@@ -5,15 +5,19 @@ import { OlStateButton } from '../components/olstatebutton';
import { faAccessibleIcon } from '@fortawesome/free-brands-svg-icons';
import { faCamera } from '@fortawesome/free-solid-svg-icons';
import { getApp } from '../../olympusapp';
import { ContextAction } from '../../unit/contextaction';
import { CONTEXT_ACTION } from '../../constants/constants';
import { FaInfoCircle, FaQuestionCircle } from 'react-icons/fa';
export function UnitMouseControlBar(props: {
}) {
var [open, setOpen] = useState(false);
var [open, setOpen] = useState(false);
var [selectedUnits, setSelectedUnits] = useState([] as Unit[]);
var [contextActionsSet, setContextActionsSet] = useState(new ContextActionSet());
var [activeContextAction, setActiveContextAction] = useState(null as null | ContextAction);
/* When a unit is selected, open the menu */
/* When a unit is selected, open the menu */
document.addEventListener("unitsSelection", (ev: CustomEventInit) => {
setOpen(true);
setSelectedUnits(ev.detail as Unit[]);
@@ -34,22 +38,43 @@ export function UnitMouseControlBar(props: {
updateData();
})
/* Update the current values of the shown data */
/* Update the current values of the shown data */
function updateData() {
var newContextActionSet = new ContextActionSet();
getApp().getUnitsManager().getSelectedUnits().forEach((unit: Unit) => {
unit.appendContextActions(newContextActionSet);
})
getApp().getUnitsManager().getSelectedUnits().forEach((unit: Unit) => {
unit.appendContextActions(newContextActionSet);
})
setContextActionsSet(newContextActionSet);
}
setActiveContextAction(null);
}
return <div className='flex gap-2 rounded-md absolute top-20 left-[50%] translate-x-[-50%] bg-gray-200 dark:bg-olympus-900 z-ui-1 p-2'>
{
Object.values(contextActionsSet.getContextActions()).map((contextAction) => {
return <OlStateButton checked={false} icon={contextAction.getIcon()} onClick={() => {}} />
})
return <> {
open && <>
<div className='flex gap-2 rounded-md absolute top-20 left-[50%] translate-x-[-50%] bg-gray-200 dark:bg-olympus-900 z-ui-1 p-2'>
{
Object.values(contextActionsSet.getContextActions()).map((contextAction) => {
return <OlStateButton checked={contextAction === activeContextAction} icon={contextAction.getIcon()} tooltip={contextAction.getLabel()} onClick={() => {
if (contextAction.getOptions().executeImmediately) {
setActiveContextAction(null);
contextAction.executeCallback(null, null);
} else {
setActiveContextAction(contextAction);
getApp().getMap().setState(CONTEXT_ACTION, { contextAction: contextAction });
}
}} />
})
}
</div>
{ activeContextAction && <div className='flex gap-2 p-4 items-center rounded-md absolute top-36 left-[50%] translate-x-[-50%] bg-gray-200 dark:bg-olympus-800 z-ui-1'>
<FaInfoCircle className="text-blue-500 mr-2 text-sm"/>
<div className='border-l-[1px] px-5 dark:text-gray-400'>
{activeContextAction.getDescription()}
</div>
</div>
}
</div>
</>
}
</>
}