microsoft/qdk

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
iadavis/qdk_openqasm_parser

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

335lines · 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";
8import {
9 isValidAngleExpression,
10 normalizeAngleExpression,
11} from "./angleExpression.js";
12
13/**
14 * Adds a context menu to a host element in the circuit visualization.
15 *
16 * @param circuitEvents The CircuitEvents instance to handle circuit-related events.
17 * @param hostElem The SVG element representing a gate component to which the context menu will be added.
18 */
19const addContextMenuToHostElem = (
20 circuitEvents: CircuitEvents,
21 hostElem: SVGGraphicsElement,
22) => {
23 hostElem?.addEventListener("contextmenu", (ev: MouseEvent) => {
24 ev.preventDefault();
25
26 // Remove any existing context menu
27 const existingContextMenu = document.querySelector(".context-menu");
28 if (existingContextMenu) {
29 document.body.removeChild(existingContextMenu);
30 }
31
32 const gateElem = findGateElem(hostElem);
33 if (!gateElem) return;
34 const selectedLocation = gateElem.getAttribute("data-location");
35 const selectedOperation = findOperation(
36 circuitEvents.componentGrid,
37 selectedLocation,
38 );
39 if (!selectedOperation || !selectedLocation) return;
40
41 const contextMenu = document.createElement("div");
42 contextMenu.classList.add("context-menu");
43 contextMenu.style.top = `${ev.clientY + window.scrollY}px`;
44 contextMenu.style.left = `${ev.clientX + window.scrollX}px`;
45 contextMenu.addEventListener("contextmenu", (e) => {
46 e.preventDefault();
47 e.stopPropagation();
48 });
49 contextMenu.addEventListener("mouseup", (e) => {
50 e.preventDefault();
51 e.stopPropagation();
52 });
53
54 const dataWireStr = hostElem.getAttribute("data-wire");
55 const dataWire = dataWireStr != null ? parseInt(dataWireStr) : null;
56 const isControl =
57 hostElem.classList.contains("control-dot") && dataWire != null;
58
59 const deleteOption = _createContextMenuItem("Delete", () => {
60 removeOperation(circuitEvents, selectedLocation);
61 circuitEvents.renderFn();
62 });
63
64 if (
65 selectedOperation.kind === "measurement" ||
66 selectedOperation.kind === "ket"
67 ) {
68 contextMenu.appendChild(deleteOption);
69 } else if (isControl) {
70 const removeControlOption = _createContextMenuItem(
71 "Remove control",
72 () => {
73 removeControl(circuitEvents, selectedOperation, dataWire);
74 circuitEvents.renderFn();
75 },
76 );
77 contextMenu.appendChild(removeControlOption!);
78 } else {
79 const adjointOption = _createContextMenuItem("Toggle Adjoint", () => {
80 if (selectedOperation.kind !== "unitary") return;
81 selectedOperation.isAdjoint = !selectedOperation.isAdjoint;
82 circuitEvents.renderFn();
83 });
84
85 const addControlOption = _createContextMenuItem("Add Control", () => {
86 if (selectedOperation.kind !== "unitary") return;
87 circuitEvents._startAddingControl(selectedOperation, selectedLocation);
88 });
89
90 let removeControlOption: HTMLDivElement | undefined;
91 if (selectedOperation.controls && selectedOperation.controls.length > 0) {
92 removeControlOption = _createContextMenuItem("Remove Control", () => {
93 circuitEvents._startRemovingControl(selectedOperation);
94 });
95 contextMenu.appendChild(removeControlOption);
96 }
97
98 const promptArgOption = _createContextMenuItem("Edit Argument", () => {
99 promptForArguments(
100 selectedOperation.params!,
101 selectedOperation.args,
102 ).then((args) => {
103 if (args.length > 0) {
104 selectedOperation.args = args;
105 } else {
106 selectedOperation.args = undefined;
107 }
108 circuitEvents.renderFn();
109 });
110 });
111
112 if (selectedOperation.gate == "X") {
113 contextMenu.appendChild(addControlOption);
114 if (removeControlOption) {
115 contextMenu.appendChild(removeControlOption);
116 }
117 contextMenu.appendChild(deleteOption);
118 } else {
119 contextMenu.appendChild(adjointOption);
120 contextMenu.appendChild(addControlOption);
121 if (removeControlOption) {
122 contextMenu.appendChild(removeControlOption);
123 }
124 if (
125 selectedOperation.params !== undefined &&
126 selectedOperation.params.length > 0
127 ) {
128 contextMenu.appendChild(promptArgOption);
129 }
130 contextMenu.appendChild(deleteOption);
131 }
132 }
133
134 document.body.appendChild(contextMenu);
135
136 document.addEventListener(
137 "click",
138 () => {
139 if (document.body.contains(contextMenu)) {
140 document.body.removeChild(contextMenu);
141 }
142 },
143 { once: true },
144 );
145 });
146};
147
148/**
149 * Prompt the user for argument values.
150 * @param params - The parameters for which the user needs to provide values.
151 * @param defaultArgs - The default values for the parameters, if any.
152 * @returns A Promise that resolves with the user-provided arguments as an array of strings.
153 */
154const promptForArguments = (
155 params: Parameter[],
156 defaultArgs: string[] = [],
157): Promise<string[]> => {
158 return new Promise((resolve) => {
159 const collectedArgs: string[] = [];
160 let currentIndex = 0;
161
162 const promptNext = () => {
163 if (currentIndex >= params.length) {
164 resolve(collectedArgs);
165 return;
166 }
167
168 const param = params[currentIndex];
169 const defaultValue = defaultArgs[currentIndex] || "";
170
171 _createInputPrompt(
172 `Enter value for parameter "${param.name}":`,
173 (userInput) => {
174 if (userInput !== null) {
175 collectedArgs.push(userInput);
176 currentIndex++;
177 promptNext();
178 } else {
179 resolve(defaultArgs); // User canceled the prompt
180 }
181 },
182 defaultValue,
183 isValidAngleExpression,
184 'Examples: "2.0 * π" or "π / 2.0"',
185 );
186 };
187
188 promptNext();
189 });
190};
191
192/**
193 * Create a context menu item
194 * @param text - The text to display in the menu item
195 * @param onClick - The function to call when the menu item is clicked
196 * @returns The created menu item element
197 */
198const _createContextMenuItem = (
199 text: string,
200 onClick: () => void,
201): HTMLDivElement => {
202 const menuItem = document.createElement("div");
203 menuItem.classList.add("context-menu-option");
204 menuItem.textContent = text;
205 menuItem.addEventListener("click", onClick);
206 return menuItem;
207};
208
209/**
210 * Create a user input prompt element
211 * @param message - The message to display in the prompt
212 * @param callback - The callback function to handle the user input
213 * @param defaultValue - The default value to display in the input element
214 * @param validateInput - A function to validate the user input
215 * @param placeholder - The placeholder text for the input element
216 */
217const _createInputPrompt = (
218 message: string,
219 callback: (input: string | null) => void,
220 defaultValue: string = "",
221 validateInput: (input: string) => boolean = () => true,
222 placeholder: string = "",
223) => {
224 // Create the prompt overlay
225 const overlay = document.createElement("div");
226 overlay.classList.add("prompt-overlay");
227 overlay.addEventListener("contextmenu", (e) => {
228 e.preventDefault();
229 e.stopPropagation();
230 });
231
232 // Create the prompt container
233 const promptContainer = document.createElement("div");
234 promptContainer.classList.add("prompt-container");
235
236 // Create the message element
237 const messageElem = document.createElement("div");
238 messageElem.classList.add("prompt-message");
239 messageElem.textContent = message;
240
241 // Create the input element
242 const inputElem = document.createElement("input");
243 inputElem.classList.add("prompt-input");
244 inputElem.type = "text";
245 inputElem.value = defaultValue;
246 inputElem.placeholder = placeholder;
247
248 // Create the buttons container
249 const buttonsContainer = document.createElement("div");
250 buttonsContainer.classList.add("prompt-buttons");
251
252 // Create the OK button
253 const okButton = document.createElement("button");
254 okButton.classList.add("prompt-button");
255 okButton.textContent = "OK";
256
257 // Function to validate input and toggle the OK button
258 const validateAndToggleOkButton = () => {
259 const processedInput = normalizeAngleExpression(inputElem.value);
260 const isValid = validateInput(processedInput);
261 okButton.disabled = !isValid;
262 };
263
264 // Add input event listener for validation
265 inputElem.addEventListener("input", validateAndToggleOkButton);
266
267 // Handle Enter key when input is focused
268 inputElem.addEventListener("keydown", (event) => {
269 if (event.key === "Enter" && !okButton.disabled) {
270 event.preventDefault();
271 okButton.click();
272 }
273 });
274
275 okButton.disabled = !validateInput(normalizeAngleExpression(defaultValue));
276 okButton.addEventListener("click", () => {
277 callback(normalizeAngleExpression(inputElem.value));
278 document.body.removeChild(overlay);
279 document.removeEventListener("keydown", handleGlobalKeyDown, true);
280 });
281
282 // Create the π button
283 const piButton = document.createElement("button");
284 piButton.textContent = "π";
285 piButton.classList.add("pi-button", "prompt-button");
286 piButton.addEventListener("click", () => {
287 const cursorPosition = inputElem.selectionStart || 0;
288 const textBefore = inputElem.value.substring(0, cursorPosition);
289 const textAfter = inputElem.value.substring(cursorPosition);
290 inputElem.value = `${textBefore}π${textAfter}`;
291 inputElem.focus();
292 inputElem.setSelectionRange(cursorPosition + 1, cursorPosition + 1); // Move cursor after "π"
293 validateAndToggleOkButton();
294 });
295
296 // Create the Cancel button
297 const cancelButton = document.createElement("button");
298 cancelButton.classList.add("prompt-button");
299 cancelButton.textContent = "Cancel";
300 cancelButton.addEventListener("click", () => {
301 callback(null);
302 document.body.removeChild(overlay);
303 document.removeEventListener("keydown", handleGlobalKeyDown, true);
304 });
305
306 // Handle Escape key globally while prompt is open
307 const handleGlobalKeyDown = (event: KeyboardEvent) => {
308 if (event.key === "Escape") {
309 event.preventDefault();
310 cancelButton.click();
311 }
312 };
313 document.addEventListener("keydown", handleGlobalKeyDown, true);
314
315 // Append buttons to the container
316 buttonsContainer.appendChild(piButton);
317 buttonsContainer.appendChild(okButton);
318 buttonsContainer.appendChild(cancelButton);
319
320 // Append elements to the prompt container
321 promptContainer.appendChild(messageElem);
322 promptContainer.appendChild(inputElem);
323 promptContainer.appendChild(buttonsContainer);
324
325 // Append the prompt container to the overlay
326 overlay.appendChild(promptContainer);
327
328 // Append the overlay to the document body
329 document.body.appendChild(overlay);
330
331 // Focus the input element
332 inputElem.focus();
333};
334
335export { addContextMenuToHostElem, promptForArguments };
336