feat(editor): the simulation's properties in a sheet of their own, with a restart for everyone
Who started the simulation, "Pakete verwerfen" and the dropped packets move from the toolbar into a properties sheet beside the surface. New in it: restart the simulation, after asking. The room learns a restart message: stopped as by stop, every capture, dropped frame and program window emptied, started anew and named after who restarted; refused for readers and frozen projects, logged, and waiting for asked blobs like a start. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -69,7 +69,7 @@ import { translate, useLanguage, useTranslate } from './i18n/i18n.js';
|
||||
import { CaptureSheet } from './simulation/CaptureSheet.js';
|
||||
import { captureWords, reportTraffic } from './simulation/captureCells.js';
|
||||
import { ModeBar, type EditorMode } from './simulation/ModeBar.js';
|
||||
import { DropControl } from './simulation/DropControl.js';
|
||||
import { PropertiesSheet } from './simulation/PropertiesSheet.js';
|
||||
import { ViewerSheet, type ViewerKind } from './simulation/ViewerSheet.js';
|
||||
import { useSeeThrough, type SheetFrame } from './simulation/SimulationSheet.js';
|
||||
import { useSimulation, type SimHostFactory } from './simulation/useSimulation.js';
|
||||
@@ -124,6 +124,9 @@ const toolButton: CSSProperties = {
|
||||
cursor: 'pointer',
|
||||
};
|
||||
|
||||
/** A tool that is on: the sheet it opens is open. */
|
||||
const pressedButton: CSSProperties = { borderColor: 'var(--accent)', background: 'var(--accent-soft)' };
|
||||
|
||||
/** Whether a key press belongs to a field being typed in. */
|
||||
const typing = (target: EventTarget | null) => target instanceof HTMLElement && (target.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName));
|
||||
|
||||
@@ -175,6 +178,8 @@ export function Editor({ doc, createSimHost, projectName, readOnly = false, froz
|
||||
const [sheet, setSheet] = useState<OpenSheet | null>(null);
|
||||
const [seeThrough, setSeeThrough] = useSeeThrough();
|
||||
const [barFolded, foldBar] = useBarFolded();
|
||||
// The simulation's „Eigenschaften“ beside the surface (PropertiesSheet).
|
||||
const [properties, setProperties] = useState(false);
|
||||
// The sheet begins below the toolbar, which wraps on a narrow screen.
|
||||
const toolbar = useRef<HTMLDivElement>(null);
|
||||
const [sheetTop, setSheetTop] = useState(68);
|
||||
@@ -512,18 +517,17 @@ export function Editor({ doc, createSimHost, projectName, readOnly = false, froz
|
||||
/>
|
||||
<div role="toolbar" className="editor-toolbar" ref={toolbar}>
|
||||
<ModeBar mode={editorMode} speed={simulation.speed} onMode={changeMode} onSpeed={simulation.setSpeed} canRun={!readOnly} />
|
||||
{simulating && simulation.startedBy !== undefined ? (
|
||||
<span className="editor-note" data-note="startedBy">
|
||||
{t('access.startedBy', { name: simulation.startedBy })}
|
||||
</span>
|
||||
) : null}
|
||||
<PeopleRow people={people} />
|
||||
{readOnly ? (
|
||||
<span className="editor-note" data-note="readOnly">
|
||||
{t(frozen ? 'access.frozen' : 'access.readOnly')}
|
||||
</span>
|
||||
) : null}
|
||||
{simulating ? (readOnly ? null : <DropControl simulation={simulation} />) : readOnly ? null : (
|
||||
{simulating ? (
|
||||
<button type="button" style={properties ? { ...toolButton, ...pressedButton } : toolButton} aria-pressed={properties} onClick={() => setProperties(!properties)}>
|
||||
{t('properties.title')}
|
||||
</button>
|
||||
) : readOnly ? null : (
|
||||
<>
|
||||
<button type="button" style={toolButton} disabled={!canUndo} onClick={() => manager?.undo()} aria-label={t('design.undo')} title={t('design.undo')}>
|
||||
↶
|
||||
@@ -637,6 +641,18 @@ export function Editor({ doc, createSimHost, projectName, readOnly = false, froz
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{simulating && properties ? (
|
||||
<PropertiesSheet
|
||||
simulation={simulation}
|
||||
readOnly={readOnly}
|
||||
onClose={() => setProperties(false)}
|
||||
onRestart={() => {
|
||||
// The data exchange's tables begin anew, with nothing chosen, as after „Tabellen löschen“.
|
||||
setCaptureTabs((before) => ({ ...before, tabs: before.tabs.map((tab) => ({ ...tab, chosen: undefined })) }));
|
||||
simulation.restart(language);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{/* Java shows the settings of the device that is chosen (not in the simulation: the design is locked); closing them lets go of it, as a click beside it does. */}
|
||||
{selected === undefined || simulating ? null : (
|
||||
<DeviceSheet
|
||||
|
||||
@@ -1320,8 +1320,7 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.viewer-bar button,
|
||||
.drop-control button {
|
||||
.viewer-bar button {
|
||||
min-height: 44px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--border-default);
|
||||
@@ -1332,47 +1331,6 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* "Pakete verwerfen" (SimulationControl): held, then the list of dropped frames below. */
|
||||
.drop-control {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.drop-control-bar {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.drop-control-hold {
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
.drop-control button[aria-pressed='true'] {
|
||||
background: var(--danger-soft);
|
||||
border-color: var(--danger);
|
||||
color: var(--danger-on-soft);
|
||||
}
|
||||
|
||||
.drop-control-list {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
left: 0;
|
||||
width: min(720px, calc(100vw - 32px));
|
||||
max-height: 240px;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
padding: 6px;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: 8px;
|
||||
background: var(--surface-card);
|
||||
color: var(--text-primary);
|
||||
font: 12px ui-monospace, Menlo, Consolas, monospace;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* The data exchange (AggregatedExchangeDialog): the tabs, a bar for what
|
||||
* Java's right mouse button offers, the table (a row per frame in its layer's
|
||||
* colour, Java's fonts by column) and the chosen frame's details below. */
|
||||
|
||||
@@ -50,11 +50,11 @@
|
||||
"connections.nic": "NIC {number}: {address}",
|
||||
"connections.remove": "Schnittstelle entfernen",
|
||||
"design.actions": "Aktionen",
|
||||
"design.palette": "Komponenten",
|
||||
"design.foldBar": "Leiste einklappen",
|
||||
"design.unfoldBar": "Leiste ausklappen",
|
||||
"design.palette": "Komponenten",
|
||||
"design.redo": "Wiederholen",
|
||||
"design.undo": "Rückgängig",
|
||||
"design.unfoldBar": "Leiste ausklappen",
|
||||
"desktop.home": "Startbildschirm",
|
||||
"desktop.installation": "Software-Installation",
|
||||
"desktop.network": "Netzwerk",
|
||||
@@ -129,7 +129,6 @@
|
||||
"docu.swatch": "Farbe {index}",
|
||||
"docu.text": "Text",
|
||||
"drop.hold": "Pakete verwerfen",
|
||||
"drop.list": "Verworfen: {count}",
|
||||
"drop.listLabel": "Verworfene Pakete",
|
||||
"echoServer.log": "Ausgabe",
|
||||
"echoServer.port": "Port:",
|
||||
@@ -363,6 +362,15 @@
|
||||
"program.textEditor": "Text-Editor",
|
||||
"program.webBrowser": "Webbrowser",
|
||||
"program.webServer": "Webserver",
|
||||
"properties.dropHint": "Solange der Knopf gedrückt ist, verwirft jedes Kabel, was es gerade überträgt.",
|
||||
"properties.droppedNone": "Noch keine verworfenen Pakete.",
|
||||
"properties.restart": "Simulation neu starten",
|
||||
"properties.restartAsk": "Die Simulation für alle neu starten? Alle Protokolle und Datenpakete gehen verloren.",
|
||||
"properties.restartHint": "Beendet die Simulation und startet sie neu, für alle im Projekt: Fenster der Programme, Datenaustausch und verworfene Pakete beginnen leer. Was die Geräte gespeichert haben, bleibt.",
|
||||
"properties.restartNo": "Abbrechen",
|
||||
"properties.restartYes": "Neu starten",
|
||||
"properties.simulation": "Simulation",
|
||||
"properties.title": "Eigenschaften",
|
||||
"remote.connecting": "Verbindung zum Projekt …",
|
||||
"remote.deleted": "Das Projekt wurde gelöscht.",
|
||||
"remote.noAccess": "Sie haben keinen Zugang zu diesem Projekt.",
|
||||
@@ -411,7 +419,6 @@
|
||||
"sheet.cards": "Netzwerkkarten",
|
||||
"sheet.chooseNetwork": "Bitte auswählen",
|
||||
"sheet.close": "Schließen",
|
||||
"sheet.seeThrough": "Durchsichtig",
|
||||
"sheet.cloud": "als Wolke darstellen",
|
||||
"sheet.connectedTo": "Verbunden mit {device}",
|
||||
"sheet.connection": "Anschluss",
|
||||
@@ -442,6 +449,7 @@
|
||||
"sheet.portForwarding": "Portfreigaben",
|
||||
"sheet.retention": "Gültigkeitsdauer SAT-Einträge (in Sek.)",
|
||||
"sheet.rip": "Automatisches Routing",
|
||||
"sheet.seeThrough": "Durchsichtig",
|
||||
"sheet.showAllEntries": "Alle Einträge anzeigen",
|
||||
"sheet.subnetMask": "Netzmaske",
|
||||
"sheet.switchSsid": "Name WLAN (SSID)",
|
||||
|
||||
@@ -50,11 +50,11 @@
|
||||
"connections.nic": "NIC {number}: {address}",
|
||||
"connections.remove": "Remove interface",
|
||||
"design.actions": "Actions",
|
||||
"design.palette": "Components",
|
||||
"design.foldBar": "Collapse the bar",
|
||||
"design.unfoldBar": "Expand the bar",
|
||||
"design.palette": "Components",
|
||||
"design.redo": "Redo",
|
||||
"design.undo": "Undo",
|
||||
"design.unfoldBar": "Expand the bar",
|
||||
"desktop.home": "Home Screen",
|
||||
"desktop.installation": "Software Installation",
|
||||
"desktop.network": "Network",
|
||||
@@ -129,7 +129,6 @@
|
||||
"docu.swatch": "Colour {index}",
|
||||
"docu.text": "Text",
|
||||
"drop.hold": "Drop Packets",
|
||||
"drop.list": "Dropped: {count}",
|
||||
"drop.listLabel": "Dropped packets",
|
||||
"echoServer.log": "Output",
|
||||
"echoServer.port": "Port:",
|
||||
@@ -363,6 +362,15 @@
|
||||
"program.textEditor": "Text editor",
|
||||
"program.webBrowser": "Webbrowser",
|
||||
"program.webServer": "Webserver",
|
||||
"properties.dropHint": "While the button is held, every cable drops what it is carrying.",
|
||||
"properties.droppedNone": "No dropped packets yet.",
|
||||
"properties.restart": "Restart simulation",
|
||||
"properties.restartAsk": "Restart the simulation for everyone? All logs and data packets are lost.",
|
||||
"properties.restartHint": "Ends the simulation and starts it again, for everyone in the project: the programs' windows, the data exchange and the dropped packets begin empty. What the devices saved stays.",
|
||||
"properties.restartNo": "Cancel",
|
||||
"properties.restartYes": "Restart",
|
||||
"properties.simulation": "Simulation",
|
||||
"properties.title": "Properties",
|
||||
"remote.connecting": "Connecting to the project …",
|
||||
"remote.deleted": "The project was deleted.",
|
||||
"remote.noAccess": "You have no access to this project.",
|
||||
@@ -411,7 +419,6 @@
|
||||
"sheet.cards": "Network cards",
|
||||
"sheet.chooseNetwork": "Please select",
|
||||
"sheet.close": "Close",
|
||||
"sheet.seeThrough": "See-through",
|
||||
"sheet.cloud": "Display as Cloud",
|
||||
"sheet.connectedTo": "Connected to {device}",
|
||||
"sheet.connection": "Connection",
|
||||
@@ -442,6 +449,7 @@
|
||||
"sheet.portForwarding": "Port Mapping",
|
||||
"sheet.retention": "Retention Time SAT Entries (Seconds)",
|
||||
"sheet.rip": "Automatic Routing",
|
||||
"sheet.seeThrough": "See-through",
|
||||
"sheet.showAllEntries": "Show all entries",
|
||||
"sheet.subnetMask": "Netmask",
|
||||
"sheet.switchSsid": "Wi-Fi Name (SSID)",
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
}
|
||||
|
||||
/* The mode buttons (and the design's undo and redo, the simulation's
|
||||
* "Pakete verwerfen") over the surface: they wrap on a narrow screen; the
|
||||
* „Eigenschaften“) over the surface: they wrap on a narrow screen; the
|
||||
* space between them lets taps through to the surface. */
|
||||
.editor-toolbar {
|
||||
position: absolute;
|
||||
@@ -78,7 +78,7 @@
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* A word beside the mode buttons: who started the simulation, that this person only reads. */
|
||||
/* A word beside the mode buttons: that this person only reads. */
|
||||
.editor-note {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -534,3 +534,49 @@
|
||||
background: var(--danger-soft);
|
||||
color: var(--danger-on-soft);
|
||||
}
|
||||
|
||||
/* The simulation's „Eigenschaften“ (PropertiesSheet): its two parts apart,
|
||||
* "Pakete verwerfen" held red, the dropped frames in Java's monospace. */
|
||||
.properties-sheet section + section {
|
||||
margin-top: 20px;
|
||||
padding-top: 4px;
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.properties-confirm p {
|
||||
margin: 8px 0 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sheet-buttons .properties-restart {
|
||||
background: var(--danger-soft);
|
||||
border-color: var(--danger);
|
||||
color: var(--danger-on-soft);
|
||||
}
|
||||
|
||||
.drop-hold {
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
.sheet-buttons .drop-hold[aria-pressed='true'] {
|
||||
background: var(--danger-soft);
|
||||
border-color: var(--danger);
|
||||
color: var(--danger-on-soft);
|
||||
}
|
||||
|
||||
.dropped-list {
|
||||
max-height: 320px;
|
||||
margin: 8px 0 0;
|
||||
overflow: auto;
|
||||
padding: 6px;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: 8px;
|
||||
background: var(--surface-raised);
|
||||
color: var(--text-primary);
|
||||
font: 12px ui-monospace, Menlo, Consolas, monospace;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
// Java's "Pakete verwerfen" (SimulationControl): while the button is held,
|
||||
// every cable drops what it carries; below it, the frames dropped in this
|
||||
// simulation, each with its time of day and Java's words for it
|
||||
// (EthernetFrame.toString). The button is held with a finger, the mouse, or
|
||||
// Space or Enter; letting go (it lets the captured pointer go), a cancel or
|
||||
// leaving the button by keyboard ends it.
|
||||
//
|
||||
// Better than Java: the list folds away and shows how many it holds (Java's
|
||||
// panel takes the bottom of the window).
|
||||
|
||||
import { useEffect, useRef, useState, type KeyboardEvent, type PointerEvent as ReactPointerEvent } from 'react';
|
||||
import { useTranslate } from '../i18n/i18n.js';
|
||||
import { timeOfDay } from './timeOfDay.js';
|
||||
import type { Simulation } from './useSimulation.js';
|
||||
|
||||
export function DropControl({ simulation }: { readonly simulation: Simulation }) {
|
||||
const t = useTranslate();
|
||||
const { watch, drop, dropped, clock } = simulation;
|
||||
useEffect(() => watch(['dropped']), [watch]);
|
||||
const [held, setHeld] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const holding = useRef(false);
|
||||
const hold = (on: boolean) => {
|
||||
if (holding.current === on) return;
|
||||
holding.current = on;
|
||||
setHeld(on);
|
||||
drop(on);
|
||||
};
|
||||
// Let go when the control goes (the simulation stopped while it was held).
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (holding.current) drop(false);
|
||||
},
|
||||
[drop],
|
||||
);
|
||||
const onPointerDown = (event: ReactPointerEvent<HTMLButtonElement>) => {
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
hold(true);
|
||||
};
|
||||
const onKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {
|
||||
if ((event.key === ' ' || event.key === 'Enter') && !event.repeat) {
|
||||
event.preventDefault();
|
||||
hold(true);
|
||||
}
|
||||
};
|
||||
const onKeyUp = (event: KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (event.key === ' ' || event.key === 'Enter') hold(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="drop-control">
|
||||
<div className="drop-control-bar">
|
||||
<button
|
||||
type="button"
|
||||
className="drop-control-hold"
|
||||
aria-pressed={held}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerCancel={() => hold(false)}
|
||||
onLostPointerCapture={() => hold(false)}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
onBlur={() => hold(false)}
|
||||
onContextMenu={(event) => event.preventDefault()}
|
||||
>
|
||||
{t('drop.hold')}
|
||||
</button>
|
||||
<button type="button" className="drop-control-toggle" aria-expanded={open} onClick={() => setOpen(!open)}>
|
||||
{t('drop.list', { count: dropped.length })}
|
||||
</button>
|
||||
</div>
|
||||
{open ? (
|
||||
<pre className="drop-control-list" aria-label={t('drop.listLabel')} tabIndex={0}>
|
||||
{dropped.map((unit) => `${timeOfDay(clock, unit.timeMilliseconds, true)} : ${unit.text}\n`).join('')}
|
||||
</pre>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Java's "Pakete verwerfen" (SimulationControl): while the button is held,
|
||||
// every cable drops what it carries. The button is held with a finger, the
|
||||
// mouse, or Space or Enter; letting go (it lets the captured pointer go), a
|
||||
// cancel or leaving the button by keyboard ends it, and so does the button
|
||||
// going (the sheet closed, the simulation stopped). It stands in the
|
||||
// simulation's „Eigenschaften“ (PropertiesSheet), the frames it dropped below it.
|
||||
|
||||
import { useEffect, useRef, useState, type KeyboardEvent, type PointerEvent as ReactPointerEvent } from 'react';
|
||||
import { useTranslate } from '../i18n/i18n.js';
|
||||
import type { Simulation } from './useSimulation.js';
|
||||
|
||||
export function DropHold({ drop }: { readonly drop: Simulation['drop'] }) {
|
||||
const t = useTranslate();
|
||||
const [held, setHeld] = useState(false);
|
||||
const holding = useRef(false);
|
||||
const hold = (on: boolean) => {
|
||||
if (holding.current === on) return;
|
||||
holding.current = on;
|
||||
setHeld(on);
|
||||
drop(on);
|
||||
};
|
||||
// Let go when the button goes (the sheet closed, the simulation stopped) while it was held.
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (holding.current) drop(false);
|
||||
},
|
||||
[drop],
|
||||
);
|
||||
const onPointerDown = (event: ReactPointerEvent<HTMLButtonElement>) => {
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
hold(true);
|
||||
};
|
||||
const onKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {
|
||||
if ((event.key === ' ' || event.key === 'Enter') && !event.repeat) {
|
||||
event.preventDefault();
|
||||
hold(true);
|
||||
}
|
||||
};
|
||||
const onKeyUp = (event: KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (event.key === ' ' || event.key === 'Enter') hold(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="drop-hold"
|
||||
aria-pressed={held}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerCancel={() => hold(false)}
|
||||
onLostPointerCapture={() => hold(false)}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
onBlur={() => hold(false)}
|
||||
onContextMenu={(event) => event.preventDefault()}
|
||||
>
|
||||
{t('drop.hold')}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
// The simulation's „Eigenschaften“: a sheet beside the surface, as the
|
||||
// device's settings are in the design (on a phone over the whole editor),
|
||||
// opened from the button of that name over the surface. What Java puts
|
||||
// around the surface while it simulates, and one thing Java has not:
|
||||
//
|
||||
// - who started the simulation that runs (on F; everyone in the project
|
||||
// watches the same one),
|
||||
// - „Simulation neu starten“: started anew for everyone, every program
|
||||
// window, the data exchange and the dropped frames emptied — what the
|
||||
// devices wrote stays. Java only stops and starts, and its windows and
|
||||
// tables outlive that. Asked once before, since it empties the others' too.
|
||||
// - Java's "Pakete verwerfen" (DropHold) and below it the frames it dropped in
|
||||
// this simulation, each with its time of day and Java's words for it
|
||||
// (EthernetFrame.toString); Java's panel takes the bottom of the window.
|
||||
//
|
||||
// Who only watches sees who started it and what was dropped, and neither
|
||||
// starts anew nor drops.
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslate } from "../i18n/i18n.js";
|
||||
import { DropHold } from "./DropHold.js";
|
||||
import { timeOfDay } from "./timeOfDay.js";
|
||||
import type { Simulation } from "./useSimulation.js";
|
||||
|
||||
export function PropertiesSheet({
|
||||
simulation,
|
||||
onRestart,
|
||||
onClose,
|
||||
readOnly = false,
|
||||
}: {
|
||||
readonly simulation: Simulation;
|
||||
/** „Simulation neu starten“ confirmed. */
|
||||
readonly onRestart: () => void;
|
||||
readonly onClose: () => void;
|
||||
readonly readOnly?: boolean;
|
||||
}) {
|
||||
const t = useTranslate();
|
||||
const { watch, drop, dropped, clock, startedBy } = simulation;
|
||||
useEffect(() => watch(["dropped"]), [watch]);
|
||||
const [asking, setAsking] = useState(false);
|
||||
|
||||
return (
|
||||
<aside
|
||||
className="device-sheet properties-sheet"
|
||||
aria-label={t("properties.title")}
|
||||
>
|
||||
<header>
|
||||
<h2>{t("properties.title")}</h2>
|
||||
<button
|
||||
type="button"
|
||||
className="sheet-close"
|
||||
onClick={onClose}
|
||||
aria-label={t("sheet.close")}
|
||||
title={t("sheet.close")}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<section aria-labelledby="properties-simulation">
|
||||
<h3 className="sheet-subtitle" id="properties-simulation">
|
||||
{t("properties.simulation")}
|
||||
</h3>
|
||||
{startedBy === undefined ? null : (
|
||||
<p className="sheet-note" data-note="startedBy">
|
||||
{t("access.startedBy", { name: startedBy })}
|
||||
</p>
|
||||
)}
|
||||
{readOnly ? null : asking ? (
|
||||
<div
|
||||
className="properties-confirm"
|
||||
role="group"
|
||||
aria-label={t("properties.restartAsk")}
|
||||
>
|
||||
<p>{t("properties.restartAsk")}</p>
|
||||
<div className="sheet-buttons">
|
||||
<button
|
||||
type="button"
|
||||
className="properties-restart"
|
||||
onClick={() => {
|
||||
setAsking(false);
|
||||
onRestart();
|
||||
}}
|
||||
>
|
||||
{t("properties.restartYes")}
|
||||
</button>
|
||||
<button type="button" onClick={() => setAsking(false)}>
|
||||
{t("properties.restartNo")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="sheet-buttons">
|
||||
<button type="button" onClick={() => setAsking(true)}>
|
||||
{t("properties.restart")}
|
||||
</button>
|
||||
</div>
|
||||
<p className="sheet-note">{t("properties.restartHint")}</p>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="properties-drop">
|
||||
<h3 className="sheet-subtitle" id="properties-drop">
|
||||
{t("drop.listLabel")} ({dropped.length})
|
||||
</h3>
|
||||
{readOnly ? null : (
|
||||
<>
|
||||
<div className="sheet-buttons">
|
||||
<DropHold drop={drop} />
|
||||
</div>
|
||||
<p className="sheet-note">{t("properties.dropHint")}</p>
|
||||
</>
|
||||
)}
|
||||
{dropped.length === 0 ? (
|
||||
<p className="sheet-note">{t("properties.droppedNone")}</p>
|
||||
) : (
|
||||
<pre
|
||||
className="dropped-list"
|
||||
aria-label={t("drop.listLabel")}
|
||||
tabIndex={0}
|
||||
>
|
||||
{dropped
|
||||
.map(
|
||||
(unit) =>
|
||||
`${timeOfDay(clock, unit.timeMilliseconds, true)} : ${unit.text}\n`,
|
||||
)
|
||||
.join("")}
|
||||
</pre>
|
||||
)}
|
||||
</section>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -55,6 +55,8 @@ export interface Simulation {
|
||||
/** Starts in the language the person runs FILIUS in: the routers' pages come in it, as Java's in its locale. */
|
||||
start(language?: Language): void;
|
||||
stop(): void;
|
||||
/** Starts anew for everyone: every window, list and capture emptied, what the devices wrote kept (not Java's). */
|
||||
restart(language?: Language): void;
|
||||
setSpeed(value: number): void;
|
||||
/** Java's "Pakete verwerfen": held (true) or let go (false). */
|
||||
drop(on: boolean): void;
|
||||
@@ -155,6 +157,10 @@ export function useSimulation(doc: Y.Doc, createHost: SimHostFactory = localSimH
|
||||
host.current?.send(language === undefined ? { v: 1, kind: 'start' } : { v: 1, kind: 'start', language });
|
||||
}, []);
|
||||
const stop = useCallback(() => host.current?.send({ v: 1, kind: 'stop' }), []);
|
||||
const restart = useCallback((language?: Language) => {
|
||||
setFailed(false);
|
||||
host.current?.send(language === undefined ? { v: 1, kind: 'restart' } : { v: 1, kind: 'restart', language });
|
||||
}, []);
|
||||
const setSpeed = useCallback((value: number) => host.current?.send({ v: 1, kind: 'speed', value }), []);
|
||||
const drop = useCallback((on: boolean) => host.current?.send({ v: 1, kind: 'drop', on }), []);
|
||||
const clearCapture = useCallback(() => host.current?.send({ v: 1, kind: 'clearCapture' }), []);
|
||||
@@ -211,5 +217,5 @@ export function useSimulation(doc: Y.Doc, createHost: SimHostFactory = localSimH
|
||||
host.current?.send({ v: 1, kind: 'input', node, app, action, args, reqId: crypto.randomUUID() });
|
||||
}, []);
|
||||
|
||||
return { mode, speed, activeLinks, nodes, apps, views, failed, startedBy, refusal, clock, dropped, traces, start, stop, setSpeed, drop, clearCapture, watch, captures, act };
|
||||
return { mode, speed, activeLinks, nodes, apps, views, failed, startedBy, refusal, clock, dropped, traces, start, stop, restart, setSpeed, drop, clearCapture, watch, captures, act };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user