diff --git a/glue/crumble/README.md b/glue/crumble/README.md
index f3b0d61c..7fa4b1fb 100644
--- a/glue/crumble/README.md
+++ b/glue/crumble/README.md
@@ -116,15 +116,15 @@ 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).
+- `ctrl+insert` or `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..6c0cd979 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]]
@@ -397,6 +390,19 @@ async function pasteFromClipboard(preview) {
return;
}
+ pasteTextAtFocus(text, preview);
+}
+
+/**
+ * Applies already-read clipboard text at the current focus.
+ *
+ * Text can come from navigator.clipboard for Ctrl+V, or from a browser paste
+ * event for Cmd+V. Keeping this shared avoids duplicating paste behavior.
+ *
+ * @param {!string} text
+ * @param {!boolean} preview
+ */
+function pasteTextAtFocus(text, preview) {
let pastedCircuit = Circuit.fromStimCircuit(text);
if (pastedCircuit.layers.length !== 1) {
throw new Error(text);
@@ -442,12 +448,86 @@ async function pasteFromClipboard(preview) {
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 +591,20 @@ function handleKeyboardEvent(ev) {
}
}
+document.addEventListener('paste', ev => {
+ if (!pendingMetaPaste) {
+ return;
+ }
+ clearPendingMetaPaste();
+
+ let text = ev.clipboardData.getData('text/plain');
+ if (text === '') {
+ return;
+ }
+
+ ev.preventDefault();
+ pasteTextAtFocus(text, false);
+});
document.addEventListener('keydown', handleKeyboardEvent);
document.addEventListener('keyup', handleKeyboardEvent);
@@ -532,7 +626,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..0d93536d 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(