feat(editor): a tap opens a device's menu in the simulation; undo and redo drawn
In the simulation a tap on a device chooses it and opens its menu where the finger was; the three-dot button is gone there, the long press stays. The context menu moves in from the surface's edge instead of being cut off. Undo and redo show drawn arrows: the characters took whatever font the device had and looked squashed on an iPad (the buttons measured 44 x 44 at every width; the size was not the cause). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -122,8 +122,26 @@ const toolButton: CSSProperties = {
|
||||
color: 'var(--text-primary)',
|
||||
font: 'inherit',
|
||||
cursor: 'pointer',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
};
|
||||
|
||||
/**
|
||||
* Undo's and redo's arrows, drawn: as characters (↶ ↷) each device took
|
||||
* them from whatever font it had, small and askew on an iPad.
|
||||
*/
|
||||
function HistoryIcon({ redo }: { readonly redo: boolean }) {
|
||||
return (
|
||||
<svg width={20} height={20} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<g transform={redo ? 'matrix(-1 0 0 1 24 0)' : undefined}>
|
||||
<path d="M9 14 4 9l5-5" />
|
||||
<path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** A tool that is on: the sheet it opens is open. */
|
||||
const pressedButton: CSSProperties = { borderColor: 'var(--accent)', background: 'var(--accent-soft)' };
|
||||
|
||||
@@ -369,7 +387,7 @@ export function Editor({ doc, createSimHost, projectName, readOnly = false, froz
|
||||
if (target.kind === 'doc') return [];
|
||||
if (simulating) {
|
||||
// Java's menu in the simulation: a computer's desktop, then each card's data exchange with its address. Before them,
|
||||
// what Java's click on the device opens — a switch's or home router's table —: a tap here only chooses.
|
||||
// what Java's click on the device opens — a switch's or home router's table —: a tap here opens this menu.
|
||||
if (target.kind !== 'device') return [];
|
||||
const node = view.nodes.find((each) => each.id === target.id);
|
||||
const items: MenuItem[] =
|
||||
@@ -498,7 +516,6 @@ export function Editor({ doc, createSimHost, projectName, readOnly = false, froz
|
||||
);
|
||||
}}
|
||||
activeLinks={simulation.activeLinks}
|
||||
hasMenu={(target) => menuItems(target).length > 0}
|
||||
onMenu={(target, at) => {
|
||||
if (readOnly && !simulating) return;
|
||||
if (editorMode === 'documentation') {
|
||||
@@ -530,10 +547,10 @@ export function Editor({ doc, createSimHost, projectName, readOnly = false, froz
|
||||
) : readOnly ? null : (
|
||||
<>
|
||||
<button type="button" style={toolButton} disabled={!canUndo} onClick={() => manager?.undo()} aria-label={t('design.undo')} title={t('design.undo')}>
|
||||
↶
|
||||
<HistoryIcon redo={false} />
|
||||
</button>
|
||||
<button type="button" style={toolButton} disabled={!canRedo} onClick={() => manager?.redo()} aria-label={t('design.redo')} title={t('design.redo')}>
|
||||
↷
|
||||
<HistoryIcon redo />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
// tool a drag from one device to another plugs a cable, and so do two
|
||||
// taps, one on each (Java's way).
|
||||
// - On a cable: a tap selects it, a long press opens its menu.
|
||||
// - In the simulation a tap on a device chooses it and opens its menu at
|
||||
// once (Java's click opens a desktop or a table; its menu holds both), and
|
||||
// nothing moves; there is no three-dot button.
|
||||
// - In the documentation mode the documentation items lie over the network
|
||||
// and take the pointer (Java's documentation panel moves to the top): a
|
||||
// tap gives one the focus, a drag on a rectangle (on a text: on the band
|
||||
@@ -84,10 +87,10 @@ export interface CanvasProps {
|
||||
/** A cable drawn active, as Java draws the cable of the card whose tab is open (Kabel.setAktiv). */
|
||||
readonly highlightedLink?: string | null;
|
||||
/**
|
||||
* The simulation: a tap on a device only chooses it, so that its
|
||||
* three-dot button shows as in the design (Java's click opens a desktop or
|
||||
* a table; here the menu does); a long press opens its menu; nothing
|
||||
* moves, as in Java's simulation mode. The documentation mode: see above.
|
||||
* The simulation: a tap on a device chooses it and opens its menu (Java's
|
||||
* click opens a desktop or a table; here the menu holds them), as a long
|
||||
* press does; nothing moves, as in Java's simulation mode. The
|
||||
* documentation mode: see above.
|
||||
*/
|
||||
readonly mode?: 'design' | 'simulation' | 'documentation';
|
||||
/**
|
||||
@@ -104,8 +107,6 @@ export interface CanvasProps {
|
||||
readonly renderDocText?: (item: DocumentationView, frame: ScreenFrame) => React.ReactNode;
|
||||
/** The cables that carry a frame now, drawn as Java draws them then (JCablePanel.farbeBlinken). */
|
||||
readonly activeLinks?: ReadonlySet<string>;
|
||||
/** In the simulation: whether the chosen device has a menu at all (a switch has none) — only then its three-dot button. */
|
||||
readonly hasMenu?: (target: Selection) => boolean;
|
||||
}
|
||||
|
||||
const NO_LINKS: ReadonlySet<string> = new Set();
|
||||
@@ -287,7 +288,6 @@ export const Canvas = forwardRef<CanvasHandle, CanvasProps>(function Canvas(
|
||||
locked = false,
|
||||
chosenBy = NO_CHOICES,
|
||||
activeLinks = NO_LINKS,
|
||||
hasMenu,
|
||||
onDocFrame,
|
||||
renderDocText,
|
||||
},
|
||||
@@ -598,7 +598,10 @@ export const Canvas = forwardRef<CanvasHandle, CanvasProps>(function Canvas(
|
||||
}
|
||||
if (now.longPressed) return;
|
||||
if (simulating) {
|
||||
if (!now.moved) onSelect?.({ kind: 'device', id: now.id });
|
||||
if (now.moved) return;
|
||||
const target = { kind: 'device', id: now.id } as const;
|
||||
onSelect?.(target);
|
||||
onMenu?.(target, now.start);
|
||||
return;
|
||||
}
|
||||
if (locked) {
|
||||
@@ -793,7 +796,7 @@ export const Canvas = forwardRef<CanvasHandle, CanvasProps>(function Canvas(
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{menuButton === null || selection === null || (simulating ? hasMenu?.(selection) !== true : locked) ? null : (
|
||||
{menuButton === null || selection === null || simulating || locked ? null : (
|
||||
<button
|
||||
type="button"
|
||||
className="device-menu-button"
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// The menu of a device or a cable (Java's design mode context menu,
|
||||
// GUIEvents.kontextMenueEntwurfsmodus): on a device "remove cable" (a PC or
|
||||
// notebook, which has one) or "remove all cables", and "delete"; on a cable
|
||||
// "remove cable". Opened by a long press or the three-dot button; every item
|
||||
// a 44 px target.
|
||||
// "remove cable"; in the simulation Java's menu there (a desktop, a table, a
|
||||
// data exchange). Opened by a long press or the three-dot button, in the
|
||||
// simulation by a tap; every item a 44 px target. It opens at the finger and
|
||||
// moves in from the surface's edge as far as it has to.
|
||||
|
||||
import { useEffect, useRef, type CSSProperties } from 'react';
|
||||
import { useEffect, useLayoutEffect, useRef, useState, type CSSProperties } from 'react';
|
||||
import type { Point } from '../canvas/viewport.js';
|
||||
|
||||
export interface MenuItem {
|
||||
@@ -29,6 +31,14 @@ const entry: CSSProperties = {
|
||||
|
||||
export function ContextMenu({ at, items, onClose }: { readonly at: Point; readonly items: readonly MenuItem[]; readonly onClose: () => void }) {
|
||||
const menu = useRef<HTMLDivElement>(null);
|
||||
const [place, setPlace] = useState(at);
|
||||
useLayoutEffect(() => {
|
||||
const element = menu.current;
|
||||
const room = element?.offsetParent;
|
||||
if (element === null || !(room instanceof HTMLElement)) return;
|
||||
const inside = (from: number, size: number, length: number) => Math.max(0, Math.min(from, length - size));
|
||||
setPlace({ x: inside(at.x, element.offsetWidth, room.clientWidth), y: inside(at.y, element.offsetHeight, room.clientHeight) });
|
||||
}, [at]);
|
||||
useEffect(() => {
|
||||
menu.current?.querySelector('button')?.focus();
|
||||
const away = (event: PointerEvent) => {
|
||||
@@ -50,8 +60,8 @@ export function ContextMenu({ at, items, onClose }: { readonly at: Point; readon
|
||||
role="menu"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: at.x,
|
||||
top: at.y,
|
||||
left: place.x,
|
||||
top: place.y,
|
||||
minWidth: 200,
|
||||
padding: '4px 0',
|
||||
borderRadius: 8,
|
||||
|
||||
Reference in New Issue
Block a user