diff --git a/glue/crumble/README.md b/glue/crumble/README.md
index f3b0d61c..e48e3837 100644
--- a/glue/crumble/README.md
+++ b/glue/crumble/README.md
@@ -116,15 +116,16 @@ button (now labelled "Hide Import/Export") again.
- `escape`: Unselect. Set current selection to the empty set.
- `delete`: Delete gates at current selection.
- `backspace`: Delete gates at current selection.
-- `ctrl+delete`: Delete current circuit layer.
+- `ctrl+delete` or `cmd+delete`: Delete current circuit layer.
- `ctrl+backspace`: Delete current circuit layer.
- `ctrl+insert`: Insert empty layer at current circuit layer, pushing current circuit layer ahead in time.
-- `ctrl+z`: Undo
-- `ctrl+y`: Redo
-- `ctrl+shift+z`: Redo
-- `ctrl+c`: Copy selection to clipboard (or entire layer if nothing selected).
-- `ctrl+v`: Past clipboard contents at current selection (or entire layer if nothing selected).
-- `ctrl+x`: Cut selection to clipboard (or entire layer if nothing selected).
+- `cmd+enter`: Insert empty layer at current circuit layer, pushing current circuit layer ahead in time.
+- `ctrl+z` or `cmd+z`: Undo
+- `ctrl+y` or `cmd+y`: Redo
+- `ctrl+shift+z` or `cmd+shift+z`: Redo
+- `ctrl+c` or `cmd+c`: Copy selection to clipboard (or entire layer if nothing selected).
+- `ctrl+v` or `cmd+v`: Paste clipboard contents at current selection (or entire layer if nothing selected).
+- `ctrl+x` or `cmd+x`: Cut selection to clipboard (or entire layer if nothing selected).
- `f`: Reverse direction of selected two qubit gates (e.g. exchange the controls and targets of a CNOT).
- `g`: Reverse order of circuit layers, from the current layer to the next empty layer.
- `home`: Jump to the first layer of the circuit.
diff --git a/glue/crumble/crumble.html b/glue/crumble/crumble.html
index ebb6189a..5415f119 100644
--- a/glue/crumble/crumble.html
+++ b/glue/crumble/crumble.html
@@ -118,8 +118,8 @@
-
-
+
+
@@ -134,8 +134,8 @@
-
-
+
+
diff --git a/glue/crumble/keyboard/toolbox.js b/glue/crumble/keyboard/toolbox.js
index 0334b2f7..557064df 100644
--- a/glue/crumble/keyboard/toolbox.js
+++ b/glue/crumble/keyboard/toolbox.js
@@ -14,7 +14,7 @@ let DEF_ROW = [1, 2, 2, 2, 2, 0, 2, 2, 2, -1, -1, -1];
* @returns {undefined|!{row: !int, strength: !number}}
*/
function getFocusedRow(ev) {
- if (ev.ctrlKey) {
+ if (ev.ctrlKey || ev.metaKey) {
return undefined;
}
let hasX = +ev.chord.has('x');
@@ -36,7 +36,7 @@ function getFocusedRow(ev) {
* @returns {undefined|!{col: !int, strength: !number}}
*/
function getFocusedCol(ev) {
- if (ev.ctrlKey) {
+ if (ev.ctrlKey || ev.metaKey) {
return undefined;
}
let best = undefined;
diff --git a/glue/crumble/main.js b/glue/crumble/main.js
index 6a14d452..dcf28bac 100644
--- a/glue/crumble/main.js
+++ b/glue/crumble/main.js
@@ -197,7 +197,7 @@ editorState.canvas.addEventListener('mouseup', ev => {
editorState.mouseDownY = undefined;
editorState.curMouseX = ev.offsetX + OFFSET_X;
editorState.curMouseY = ev.offsetY + OFFSET_Y;
- editorState.changeFocus(highlightedArea, ev.shiftKey, ev.ctrlKey);
+ editorState.changeFocus(highlightedArea, ev.shiftKey, ev.ctrlKey || ev.metaKey);
if (ev.buttons === 1) {
isInScrubber = false;
}
@@ -222,17 +222,8 @@ function makeChordHandlers() {
res.set('ctrl+shift+z', preview => { if (!preview) editorState.redo() });
res.set('ctrl+c', async preview => { await copyToClipboard(); });
res.set('ctrl+v', pasteFromClipboard);
- res.set('ctrl+x', async preview => {
- await copyToClipboard();
- if (editorState.focusedSet.size === 0) {
- let c = editorState.copyOfCurCircuit();
- c.layers[editorState.curLayer].id_ops.clear();
- c.layers[editorState.curLayer].markers.length = 0;
- editorState.commit_or_preview(c, preview);
- } else {
- editorState.deleteAtFocus(preview);
- }
- });
+ res.set('ctrl+x', cutToClipboard);
+
res.set('l', preview => {
if (!preview) {
editorState.timelineSet = new Map(editorState.focusedSet.entries());
@@ -360,6 +351,8 @@ function makeChordHandlers() {
}
let fallbackEmulatedClipboard = undefined;
+let pendingMetaPaste = false;
+let pendingMetaPasteTimeout = undefined;
async function copyToClipboard() {
let c = editorState.copyOfCurCircuit();
c.layers = [c.layers[editorState.curLayer]]
@@ -442,12 +435,136 @@ async function pasteFromClipboard(preview) {
editorState.commit_or_preview(newCircuit, preview);
}
+/**
+ * @param {!string} text
+ * @param {!boolean} preview
+ */
+function pasteTextFromClipboardEvent(text, preview) {
+ let pastedCircuit = Circuit.fromStimCircuit(text);
+ if (pastedCircuit.layers.length !== 1) {
+ throw new Error(text);
+ }
+ let newCircuit = editorState.copyOfCurCircuit();
+ if (editorState.focusedSet.size > 0) {
+ let [x, y] = minXY(editorState.focusedSet.values());
+ pastedCircuit = pastedCircuit.shifted(x, y);
+ }
+
+ // Include new coordinates.
+ let usedCoords = [];
+ for (let q = 0; q < pastedCircuit.qubitCoordData.length; q += 2) {
+ usedCoords.push([pastedCircuit.qubitCoordData[q], pastedCircuit.qubitCoordData[q + 1]]);
+ }
+ newCircuit = newCircuit.withCoordsIncluded(usedCoords);
+ let c2q = newCircuit.coordToQubitMap();
+
+ // Remove existing content at paste location.
+ for (let key of editorState.focusedSet.keys()) {
+ let q = c2q.get(key);
+ if (q !== undefined) {
+ newCircuit.layers[editorState.curLayer].id_pop_at(q);
+ }
+ }
+
+ // Add content to paste location.
+ for (let op of pastedCircuit.layers[0].iter_gates_and_markers()) {
+ let newTargets = [];
+ for (let q of op.id_targets) {
+ let x = pastedCircuit.qubitCoordData[2*q];
+ let y = pastedCircuit.qubitCoordData[2*q+1];
+ newTargets.push(c2q.get(`${x},${y}`));
+ }
+ newCircuit.layers[editorState.curLayer].put(new Operation(
+ op.gate,
+ op.tag,
+ op.args,
+ new Uint32Array(newTargets),
+ ));
+ }
+
+ editorState.commit_or_preview(newCircuit, preview);
+}
+
+function clearPendingMetaPaste() {
+ pendingMetaPaste = false;
+ if (pendingMetaPasteTimeout !== undefined) {
+ clearTimeout(pendingMetaPasteTimeout);
+ pendingMetaPasteTimeout = undefined;
+ }
+}
+
+async function cutToClipboard(preview) {
+ await copyToClipboard();
+ if (editorState.focusedSet.size === 0) {
+ let c = editorState.copyOfCurCircuit();
+ c.layers[editorState.curLayer].id_ops.clear();
+ c.layers[editorState.curLayer].markers.length = 0;
+ editorState.commit_or_preview(c, preview);
+ } else {
+ editorState.deleteAtFocus(preview);
+ }
+}
+
const CHORD_HANDLERS = makeChordHandlers();
/**
* @param {!KeyboardEvent} ev
*/
-function handleKeyboardEvent(ev) {
+async function handleKeyboardEvent(ev) {
+ if (ev.type === 'keydown' && ev.metaKey) {
+ if (ev.repeat) {
+ ev.preventDefault();
+ editorState.chorder.handleFocusChanged();
+ return;
+ }
+
+ let key = ev.key.toLowerCase();
+
+ if (key === 'z' && !ev.shiftKey) {
+ ev.preventDefault();
+ editorState.chorder.handleFocusChanged();
+ editorState.undo();
+ return;
+ }
+ if ((key === 'z' && ev.shiftKey) || key === 'y') {
+ ev.preventDefault();
+ editorState.chorder.handleFocusChanged();
+ editorState.redo();
+ return;
+ }
+ if (key === 'c') {
+ ev.preventDefault();
+ editorState.chorder.handleFocusChanged();
+ await copyToClipboard();
+ return;
+ }
+ if (key === 'v') {
+ editorState.chorder.handleFocusChanged();
+ pendingMetaPaste = true;
+ pendingMetaPasteTimeout = setTimeout(clearPendingMetaPaste, 1000);
+ return;
+ }
+ if (key === 'x') {
+ ev.preventDefault();
+ editorState.chorder.handleFocusChanged();
+ await cutToClipboard(false);
+ return;
+ }
+ if (key === 'backspace' || key === 'delete') {
+ ev.preventDefault();
+ editorState.chorder.handleFocusChanged();
+ editorState.deleteCurLayer(false);
+ return;
+ }
+ if (key === 'enter') {
+ ev.preventDefault();
+ editorState.chorder.handleFocusChanged();
+ editorState.insertLayer(false);
+ return;
+ }
+ }
+
editorState.chorder.handleKeyEvent(ev);
+
if (ev.type === 'keydown') {
if (ev.key.toLowerCase() === 'q') {
let d = ev.shiftKey ? 5 : 1;
@@ -511,6 +628,20 @@ function handleKeyboardEvent(ev) {
}
}
+document.addEventListener('paste', ev => {
+ if (!pendingMetaPaste) {
+ return;
+ }
+ clearPendingMetaPaste();
+
+ let text = ev.clipboardData.getData('text/plain');
+ if (text === '') {
+ return;
+ }
+
+ ev.preventDefault();
+ pasteTextFromClipboardEvent(text, false);
+});
document.addEventListener('keydown', handleKeyboardEvent);
document.addEventListener('keyup', handleKeyboardEvent);
@@ -532,7 +663,7 @@ window.addEventListener('blur', () => {
for (let anchor of document.getElementById('examples-div').querySelectorAll('a')) {
anchor.onclick = ev => {
// Don't stop the user from e.g. opening the example in a new tab using ctrl+click.
- if (ev.shiftKey || ev.ctrlKey || ev.altKey || ev.button !== 0) {
+ if (ev.shiftKey || ev.ctrlKey || ev.metaKey || ev.altKey || ev.button !== 0) {
return undefined;
}
let circuitText = anchor.href.split('#circuit=')[1];
diff --git a/src/stim/diagram/crumble_data.cc b/src/stim/diagram/crumble_data.cc
index 0d11b307..022e505c 100644
--- a/src/stim/diagram/crumble_data.cc
+++ b/src/stim/diagram/crumble_data.cc
@@ -243,9 +243,9 @@ std::string stim_draw_internal::make_crumble_html() {
)CRUMBLE_PART");
result.append(R"CRUMBLE_PART(