Add hotkeys, document hotkeys, make 'DefaultGUI' runnable via NodeJS, generate hotkey documentation

This commit is contained in:
Pieter Vander Vennet 2022-12-24 03:44:21 +01:00
parent e2419c19cd
commit 43613e4ece
16 changed files with 328 additions and 89 deletions

110
UI/Base/Hotkeys.ts Normal file
View file

@ -0,0 +1,110 @@
import { Utils } from "../../Utils"
import Combine from "./Combine"
import BaseUIElement from "../BaseUIElement"
import Title from "./Title"
import Table from "./Table"
import { UIEventSource } from "../../Logic/UIEventSource"
import { VariableUiElement } from "./VariableUIElement"
export default class Hotkeys {
private static readonly _docs: UIEventSource<
{
key: { ctrl?: string; shift?: string; alt?: string; nomod?: string; onUp?: boolean }
documentation: string
}[]
> = new UIEventSource<
{
key: { ctrl?: string; shift?: string; alt?: string; nomod?: string; onUp?: boolean }
documentation: string
}[]
>([])
public static RegisterHotkey(
key: (
| {
ctrl: string
}
| {
shift: string
}
| {
alt: string
}
| {
nomod: string
}
) & {
onUp?: boolean
},
documentation: string,
action: () => void
) {
const type = key["onUp"] ? "keyup" : "keypress"
let keycode: string = key["ctrl"] ?? key["shift"] ?? key["alt"] ?? key["nomod"]
if (keycode.length == 1) {
keycode = keycode.toLowerCase()
if (key["shift"] !== undefined) {
keycode = keycode.toUpperCase()
}
}
this._docs.data.push({ key, documentation })
this._docs.ping()
if (Utils.runningFromConsole) {
return
}
if (key["ctrl"] !== undefined) {
document.addEventListener("keydown", function (event) {
if (event.ctrlKey && event.key === keycode) {
action()
event.preventDefault()
}
})
} else if (key["shift"] !== undefined) {
document.addEventListener(type, function (event) {
if (event.shiftKey && event.key === keycode) {
action()
event.preventDefault()
}
})
} else if (key["alt"] !== undefined) {
document.addEventListener(type, function (event) {
if (event.altKey && event.key === keycode) {
action()
event.preventDefault()
}
})
} else if (key["nomod"] !== undefined) {
document.addEventListener(type, function (event) {
if (event.key === keycode) {
action()
event.preventDefault()
}
})
}
}
static generateDocumentation(): BaseUIElement {
return new Combine([
new Title("Hotkeys", 1),
"MapComplete supports the following keys:",
new Table(
["Key combination", "Action"],
Hotkeys._docs.data
.map(({ key, documentation }) => {
const modifiers = Object.keys(key).filter(
(k) => k !== "nomod" && k !== "onUp"
)
const keycode: string =
key["ctrl"] ?? key["shift"] ?? key["alt"] ?? key["nomod"]
modifiers.push(keycode)
return [modifiers.join("+"), documentation]
})
.sort()
),
])
}
static generateDocumentationDynamic(): BaseUIElement {
return new VariableUiElement(Hotkeys._docs.map((_) => Hotkeys.generateDocumentation()))
}
}

View file

@ -14,7 +14,83 @@ import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"
import AvailableBaseLayersImplementation from "../../Logic/Actors/AvailableBaseLayersImplementation"
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
import ShowDataLayerImplementation from "../ShowDataLayer/ShowDataLayerImplementation"
import FilteredLayer from "../../Models/FilteredLayer"
import ScrollableFullScreen from "./ScrollableFullScreen"
import Constants from "../../Models/Constants"
import StrayClickHandler from "../../Logic/Actors/StrayClickHandler"
/**
* The stray-click-hanlders adds a marker to the map if no feature was clicked.
* Shows the given uiToShow-element in the messagebox
*/
export class StrayClickHandlerImplementation {
private _lastMarker
constructor(
state: {
LastClickLocation: UIEventSource<{ lat: number; lon: number }>
selectedElement: UIEventSource<string>
filteredLayers: UIEventSource<FilteredLayer[]>
leafletMap: UIEventSource<L.Map>
},
uiToShow: ScrollableFullScreen,
iconToShow: BaseUIElement
) {
const self = this
const leafletMap = state.leafletMap
state.filteredLayers.data.forEach((filteredLayer) => {
filteredLayer.isDisplayed.addCallback((isEnabled) => {
if (isEnabled && self._lastMarker && leafletMap.data !== undefined) {
// When a layer is activated, we remove the 'last click location' in order to force the user to reclick
// This reclick might be at a location where a feature now appeared...
state.leafletMap.data.removeLayer(self._lastMarker)
}
})
})
state.LastClickLocation.addCallback(function (lastClick) {
if (self._lastMarker !== undefined) {
state.leafletMap.data?.removeLayer(self._lastMarker)
}
if (lastClick === undefined) {
return
}
state.selectedElement.setData(undefined)
const clickCoor: [number, number] = [lastClick.lat, lastClick.lon]
self._lastMarker = L.marker(clickCoor, {
icon: L.divIcon({
html: iconToShow.ConstructElement(),
iconSize: [50, 50],
iconAnchor: [25, 50],
popupAnchor: [0, -45],
}),
})
self._lastMarker.addTo(leafletMap.data)
self._lastMarker.on("click", () => {
if (leafletMap.data.getZoom() < Constants.userJourney.minZoomLevelToAddNewPoints) {
leafletMap.data.flyTo(
clickCoor,
Constants.userJourney.minZoomLevelToAddNewPoints
)
return
}
uiToShow.Activate()
})
})
state.selectedElement.addCallback(() => {
if (self._lastMarker !== undefined) {
leafletMap.data.removeLayer(self._lastMarker)
this._lastMarker = undefined
}
})
}
}
export default class MinimapImplementation extends BaseUIElement implements MinimapObj {
private static _nextId = 0
public readonly leafletMap: UIEventSource<Map>
@ -53,6 +129,18 @@ export default class MinimapImplementation extends BaseUIElement implements Mini
AvailableBaseLayers.implement(new AvailableBaseLayersImplementation())
Minimap.createMiniMap = (options) => new MinimapImplementation(options)
ShowDataLayer.actualContstructor = (options) => new ShowDataLayerImplementation(options)
StrayClickHandler.construct = (
state: {
LastClickLocation: UIEventSource<{ lat: number; lon: number }>
selectedElement: UIEventSource<string>
filteredLayers: UIEventSource<FilteredLayer[]>
leafletMap: UIEventSource<L.Map>
},
uiToShow: ScrollableFullScreen,
iconToShow: BaseUIElement
) => {
return new StrayClickHandlerImplementation(state, uiToShow, iconToShow)
}
}
public installBounds(factor: number | BBox, showRange?: boolean) {

View file

@ -5,6 +5,7 @@ import { UIEventSource } from "../../Logic/UIEventSource"
import Hash from "../../Logic/Web/Hash"
import BaseUIElement from "../BaseUIElement"
import Title from "./Title"
import Hotkeys from "./Hotkeys"
/**
*
@ -82,12 +83,11 @@ export default class ScrollableFullScreen {
}
private static initEmpty(): FixedUiElement {
document.addEventListener("keyup", function (event) {
if (event.code === "Escape") {
ScrollableFullScreen.collapse()
event.preventDefault()
}
})
Hotkeys.RegisterHotkey(
{ nomod: "Escape", onUp: true },
"Close the sidebar",
ScrollableFullScreen.collapse
)
return new FixedUiElement("")
}
@ -117,7 +117,7 @@ export default class ScrollableFullScreen {
this._fullscreencomponent.AttachTo("fullscreen")
const fs = document.getElementById("fullscreen")
ScrollableFullScreen._currentlyOpen = this
fs.classList.remove("hidden")
fs?.classList?.remove("hidden")
}
private BuildComponent(title: BaseUIElement, content: BaseUIElement): BaseUIElement {