microsoft/qdk

Public

mirrored from https://github.com/microsoft/qdkAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.23.0

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

source/npm/qsharp/ux/circuit-vis/contextMenu.ts

400lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT license.
3
4import { Parameter } from "./circuit.js";
5import { removeControl, removeOperation } from "./circuitManipulation.js";
6import { CircuitEvents } from "./events.js";
7import { findGateElem, findOperation } from "./utils.js";
8
9/**
10 * Adds a context menu to a host element in the circuit visualization.
11 *
12 * @param circuitEvents The CircuitEvents instance to handle circuit-related events.
13 * @param hostElem The SVG element representing a gate component to which the context menu will be added.
14 */
15const addContextMenuToHostElem = (
16 circuitEvents: CircuitEvents,
17 hostElem: SVGGraphicsElement,
18) => {
19 hostElem?.addEventListener("contextmenu", (ev: MouseEvent) => {
20 ev.preventDefault();
21
22 // Remove any existing context menu
23 const existingContextMenu = document.querySelector(".context-menu");
24 if (existingContextMenu) {
25 document.body.removeChild(existingContextMenu);
26 }
27
28 const gateElem = findGateElem(hostElem);
29 if (!gateElem) return;
30 const selectedLocation = gateElem.getAttribute("data-location");
31 const selectedOperation = findOperation(
32 circuitEvents.componentGrid,
33 selectedLocation,
34 );
35 if (!selectedOperation || !selectedLocation) return;
36
37 const contextMenu = document.createElement("div");
38 contextMenu.classList.add("context-menu");
39 contextMenu.style.top = `${ev.clientY + window.scrollY}px`;
40 contextMenu.style.left = `${ev.clientX + window.scrollX}px`;
41 contextMenu.addEventListener("contextmenu", (e) => {
42 e.preventDefault();
43 e.stopPropagation();
44 });
45 contextMenu.addEventListener("mouseup", (e) => {
46 e.preventDefault();
47 e.stopPropagation();
48 });
49
50 const dataWireStr = hostElem.getAttribute("data-wire");
51 const dataWire = dataWireStr != null ? parseInt(dataWireStr) : null;
52 const isControl =
53 hostElem.classList.contains("control-dot") && dataWire != null;
54
55 const deleteOption = _createContextMenuItem("Delete", () => {
56 removeOperation(circuitEvents, selectedLocation);
57 circuitEvents.renderFn();
58 });
59
60 if (
61 selectedOperation.kind === "measurement" ||
62 selectedOperation.kind === "ket"
63 ) {
64 contextMenu.appendChild(deleteOption);
65 } else if (isControl) {
66 const removeControlOption = _createContextMenuItem(
67 "Remove control",
68 () => {
69 removeControl(circuitEvents, selectedOperation, dataWire);
70 circuitEvents.renderFn();
71 },
72 );
73 contextMenu.appendChild(removeControlOption!);
74 } else {
75 const adjointOption = _createContextMenuItem("Toggle Adjoint", () => {
76 if (selectedOperation.kind !== "unitary") return;
77 selectedOperation.isAdjoint = !selectedOperation.isAdjoint;
78 circuitEvents.renderFn();
79 });
80
81 const addControlOption = _createContextMenuItem("Add Control", () => {
82 if (selectedOperation.kind !== "unitary") return;
83 circuitEvents._startAddingControl(selectedOperation, selectedLocation);
84 });
85
86 let removeControlOption: HTMLDivElement | undefined;
87 if (selectedOperation.controls && selectedOperation.controls.length > 0) {
88 removeControlOption = _createContextMenuItem("Remove Control", () => {
89 circuitEvents._startRemovingControl(selectedOperation);
90 });
91 contextMenu.appendChild(removeControlOption);
92 }
93
94 const promptArgOption = _createContextMenuItem("Edit Argument", () => {
95 promptForArguments(
96 selectedOperation.params!,
97 selectedOperation.args,
98 ).then((args) => {
99 if (args.length > 0) {
100 selectedOperation.args = args;
101 } else {
102 selectedOperation.args = undefined;
103 }
104 circuitEvents.renderFn();
105 });
106 });
107
108 if (selectedOperation.gate == "X") {
109 contextMenu.appendChild(addControlOption);
110 if (removeControlOption) {
111 contextMenu.appendChild(removeControlOption);
112 }
113 contextMenu.appendChild(deleteOption);
114 } else {
115 contextMenu.appendChild(adjointOption);
116 contextMenu.appendChild(addControlOption);
117 if (removeControlOption) {
118 contextMenu.appendChild(removeControlOption);
119 }
120 if (
121 selectedOperation.params !== undefined &&
122 selectedOperation.params.length > 0
123 ) {
124 contextMenu.appendChild(promptArgOption);
125 }
126 contextMenu.appendChild(deleteOption);
127 }
128 }
129
130 document.body.appendChild(contextMenu);
131
132 document.addEventListener(
133 "click",
134 () => {
135 if (document.body.contains(contextMenu)) {
136 document.body.removeChild(contextMenu);
137 }
138 },
139 { once: true },
140 );
141 });
142};
143
144/**
145 * Prompt the user for argument values.
146 * @param params - The parameters for which the user needs to provide values.
147 * @param defaultArgs - The default values for the parameters, if any.
148 * @returns A Promise that resolves with the user-provided arguments as an array of strings.
149 */
150const promptForArguments = (
151 params: Parameter[],
152 defaultArgs: string[] = [],
153): Promise<string[]> => {
154 return new Promise((resolve) => {
155 const collectedArgs: string[] = [];
156 let currentIndex = 0;
157
158 const promptNext = () => {
159 if (currentIndex >= params.length) {
160 resolve(collectedArgs);
161 return;
162 }
163
164 const param = params[currentIndex];
165 const defaultValue = defaultArgs[currentIndex] || "";
166
167 _createInputPrompt(
168 `Enter value for parameter "${param.name}":`,
169 (userInput) => {
170 if (userInput !== null) {
171 collectedArgs.push(userInput);
172 currentIndex++;
173 promptNext();
174 } else {
175 resolve(defaultArgs); // User canceled the prompt
176 }
177 },
178 defaultValue,
179 validateExpression,
180 'Examples: "2.0 * π" or "π / 2.0"',
181 );
182 };
183
184 promptNext();
185 });
186};
187
188/**
189 * Validate a mathematical expression.
190 * @param input - The input string to validate.
191 * @returns True if the expression is valid, false otherwise.
192 */
193const validateExpression = (input: string): boolean => {
194 // Removes outermost parentheses
195 const removeParentheses = (expr: string): string => {
196 while (expr.startsWith("(") && expr.endsWith(")")) {
197 expr = expr.slice(1, -1).trim(); // Remove outermost parentheses
198 }
199 return expr;
200 };
201
202 // Validates parentheses balance and nesting
203 const validateParentheses = (expr: string): boolean => {
204 const stack: string[] = [];
205 for (const char of expr) {
206 if (char === "(") {
207 stack.push(char);
208 } else if (char === ")") {
209 if (stack.length === 0) {
210 return false; // Unmatched closing parenthesis
211 }
212 stack.pop();
213 }
214 }
215 return stack.length === 0; // Ensure no unmatched opening parentheses
216 };
217
218 // Validate the expression recursively
219 const validate = (expr: string): boolean => {
220 expr = expr.trim();
221
222 // Remove outermost parentheses
223 expr = removeParentheses(expr);
224
225 // Find and validate all sub-expressions within parentheses
226 const parenthesesRegex = /\(([^()]+)\)/g;
227 let match;
228 while ((match = parenthesesRegex.exec(expr)) !== null) {
229 const innerExpr = match[1];
230 if (!validate(innerExpr)) {
231 return false; // Invalid sub-expression
232 }
233
234 // Replace the validated sub-expression with a placeholder that we know is valid
235 expr = expr.replace(match[0], "π");
236 }
237
238 // Validate the remaining expression (without parentheses)
239 const sign = "[+-]?";
240 const number = "((\\d+(\\.\\d*)?))"; // Matches integers and decimals
241 const pi = "(?i:π)"; // Matches π
242 const value = `${sign}(${number}|${pi})`; // Matches a signed number or π
243 const operator = "[+\\-*/]"; // Matches arithmetic operators
244 const expressionRegex = new RegExp(
245 `^${value}(\\s*${operator}\\s*${value})*$`,
246 );
247
248 return expressionRegex.test(expr);
249 };
250
251 return validateParentheses(input) && validate(input);
252};
253
254/**
255 * Create a context menu item
256 * @param text - The text to display in the menu item
257 * @param onClick - The function to call when the menu item is clicked
258 * @returns The created menu item element
259 */
260const _createContextMenuItem = (
261 text: string,
262 onClick: () => void,
263): HTMLDivElement => {
264 const menuItem = document.createElement("div");
265 menuItem.classList.add("context-menu-option");
266 menuItem.textContent = text;
267 menuItem.addEventListener("click", onClick);
268 return menuItem;
269};
270
271/**
272 * Create a user input prompt element
273 * @param message - The message to display in the prompt
274 * @param callback - The callback function to handle the user input
275 * @param defaultValue - The default value to display in the input element
276 * @param validateInput - A function to validate the user input
277 * @param placeholder - The placeholder text for the input element
278 */
279const _createInputPrompt = (
280 message: string,
281 callback: (input: string | null) => void,
282 defaultValue: string = "",
283 validateInput: (input: string) => boolean = () => true,
284 placeholder: string = "",
285) => {
286 // Create the prompt overlay
287 const overlay = document.createElement("div");
288 overlay.classList.add("prompt-overlay");
289 overlay.addEventListener("contextmenu", (e) => {
290 e.preventDefault();
291 e.stopPropagation();
292 });
293
294 // Create the prompt container
295 const promptContainer = document.createElement("div");
296 promptContainer.classList.add("prompt-container");
297
298 // Create the message element
299 const messageElem = document.createElement("div");
300 messageElem.classList.add("prompt-message");
301 messageElem.textContent = message;
302
303 // Create the input element
304 const inputElem = document.createElement("input");
305 inputElem.classList.add("prompt-input");
306 inputElem.type = "text";
307 inputElem.value = defaultValue;
308 inputElem.placeholder = placeholder;
309
310 // Create the buttons container
311 const buttonsContainer = document.createElement("div");
312 buttonsContainer.classList.add("prompt-buttons");
313
314 // Create the OK button
315 const okButton = document.createElement("button");
316 okButton.classList.add("prompt-button");
317 okButton.textContent = "OK";
318
319 // Function to replace "pi" with "π" (case-insensitive)
320 const replacePiWithSymbol = (input: string) => input.replace(/pi/gi, "π");
321
322 // Function to validate input and toggle the OK button
323 const validateAndToggleOkButton = () => {
324 const processedInput = replacePiWithSymbol(inputElem.value.trim());
325 const isValid = validateInput(processedInput);
326 okButton.disabled = !isValid;
327 };
328
329 // Add input event listener for validation
330 inputElem.addEventListener("input", validateAndToggleOkButton);
331
332 // Handle Enter key when input is focused
333 inputElem.addEventListener("keydown", (event) => {
334 if (event.key === "Enter" && !okButton.disabled) {
335 event.preventDefault();
336 okButton.click();
337 }
338 });
339
340 okButton.disabled = !validateInput(replacePiWithSymbol(defaultValue.trim()));
341 okButton.addEventListener("click", () => {
342 callback(replacePiWithSymbol(inputElem.value.trim()));
343 document.body.removeChild(overlay);
344 document.removeEventListener("keydown", handleGlobalKeyDown, true);
345 });
346
347 // Create the π button
348 const piButton = document.createElement("button");
349 piButton.textContent = "π";
350 piButton.classList.add("pi-button", "prompt-button");
351 piButton.addEventListener("click", () => {
352 const cursorPosition = inputElem.selectionStart || 0;
353 const textBefore = inputElem.value.substring(0, cursorPosition);
354 const textAfter = inputElem.value.substring(cursorPosition);
355 inputElem.value = `${textBefore}π${textAfter}`;
356 inputElem.focus();
357 inputElem.setSelectionRange(cursorPosition + 1, cursorPosition + 1); // Move cursor after "π"
358 validateAndToggleOkButton();
359 });
360
361 // Create the Cancel button
362 const cancelButton = document.createElement("button");
363 cancelButton.classList.add("prompt-button");
364 cancelButton.textContent = "Cancel";
365 cancelButton.addEventListener("click", () => {
366 callback(null);
367 document.body.removeChild(overlay);
368 document.removeEventListener("keydown", handleGlobalKeyDown, true);
369 });
370
371 // Handle Escape key globally while prompt is open
372 const handleGlobalKeyDown = (event: KeyboardEvent) => {
373 if (event.key === "Escape") {
374 event.preventDefault();
375 cancelButton.click();
376 }
377 };
378 document.addEventListener("keydown", handleGlobalKeyDown, true);
379
380 // Append buttons to the container
381 buttonsContainer.appendChild(piButton);
382 buttonsContainer.appendChild(okButton);
383 buttonsContainer.appendChild(cancelButton);
384
385 // Append elements to the prompt container
386 promptContainer.appendChild(messageElem);
387 promptContainer.appendChild(inputElem);
388 promptContainer.appendChild(buttonsContainer);
389
390 // Append the prompt container to the overlay
391 overlay.appendChild(promptContainer);
392
393 // Append the overlay to the document body
394 document.body.appendChild(overlay);
395
396 // Focus the input element
397 inputElem.focus();
398};
399
400export { addContextMenuToHostElem, promptForArguments };
401