PageSourceSearch

https://jayreddinjr-jr-odysseus-hf.hf.space/static/js/editor/keyboard-shortcuts.js

js jayreddinjr-jr-odysseus-hf.hf.space collected 2026-10-04 17:01:17 UTC 11,916 bytes, 271 lines download raw bytes

1/**
2 * Editor keyboard shortcuts — bound to `document` so shortcuts work
3 * without first clicking into the canvas. Gated by `state.editorOpen`
4 * so they don't leak into chat input when the editor is closed.
5 *
6 * Covers:
7 *   ?              toggle the shortcuts cheatsheet
8 *   Enter          confirm in-progress transform
9 *   Esc            cancel transform / lasso / crop (in priority order)
10 *   Ctrl+Z         undo (Shift adds redo)
11 *   Ctrl+Shift+D   deselect (clears wand + lasso)
12 *   Ctrl+S         save (Shift = save as / export to gallery)
13 *   Ctrl+Shift+T   open resize popup
14 *   Ctrl+Alt+T     start free transform
15 *   Ctrl+Alt+I     invert wand / lasso selection
16 *   Ctrl+Alt+J     new empty layer
17 *   Ctrl+Alt+A     select all canvas (lasso polygon = full bounds)
18 *   Ctrl+C/X       copy / cut wand or lasso selection (image clipboard
19 *                  + internal clipboard)
20 *   Ctrl+V         (handled by the paste event listener)
21 *   Tool keys (V, B, E, L, …) → toolbar click
22 *   [ / ]          shrink / grow brush size proportionally
23 *   D, C, M (when lasso has 3+ points) → delete / copy / convert mask
24 *   Delete / Backspace (wand or lasso) → delete pixels
25 *
26 * @param {{
27 *   toolbar:                HTMLDivElement,
28 *   toolKeyMap:             Record<string, string>,
29 *   composite:              () => void,
30 *   saveState:              (label?: string) => void,
31 *   undo:                   () => void,
32 *   redo:                   () => void,
33 *   toggleShortcuts:        (show?: boolean) => void,
34 *   confirmTransform:       () => void,
35 *   cancelTransform:        () => void,
36 *   startTransform:         () => void,
37 *   resizeCustomPrompt:     () => void,
38 *   addEmptyLayer:          () => void,
39 *   brushSizeSync:          (source: HTMLInputElement | null) => void,
40 *   invertSelection:        () => boolean,
41 *   wandDeleteSelection:    () => void,
42 *   wandCopyToNewLayer:     () => void,
43 *   lassoDeleteSelection:   () => void,
44 *   lassoCopyToLayer:       () => void,
45 *   lassoToMask:            () => void,
46 *   buildLassoMask:         (w: number, h: number, offX: number, offY: number, feather: number, grow: number) => HTMLCanvasElement,
47 *   drawLassoOverlay:       () => void,
48 *   activeLayer:            () => object | null,
49 *   uiModule:               object,
50 * }} deps
51 */
52import { state } from './state.js';
53import { isAltGrEvent } from '../platform.js';
54
55export function wireKeyboardShortcuts(deps) {
56  const {
57    toolbar, toolKeyMap,
58    composite, saveState, undo, redo,
59    toggleShortcuts, confirmTransform, cancelTransform, startTransform,
60    resizeCustomPrompt, addEmptyLayer, brushSizeSync,
61    invertSelection,
62    wandDeleteSelection, wandCopyToNewLayer,
63    lassoDeleteSelection, lassoCopyToLayer, lassoToMask,
64    buildLassoMask, drawLassoOverlay,
65    activeLayer, uiModule,
66  } = deps;
67
68  document.addEventListener('keydown', (e) => {
69    if (!state.editorOpen) return;
70    // `?` toggles the cheatsheet. Don't fire while typing in a text
71    // field — the user might be typing a prompt with a `?`.
72    if (e.key === '?' && e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') {
73      e.preventDefault();
74      toggleShortcuts();
75      return;
76    }
77    if (e.key === 'Enter' && state.transformActive) {
78      e.preventDefault();
79      confirmTransform();
80      return;
81    }
82    if (e.key === 'Escape') return;
83    // Skip the Ctrl+Alt editor chords for an AltGr keystroke (see platform.js);
84    // only the chord block is skipped, so the layout-character handlers below
85    // still act — AltGr+5 / AltGr+8 stay as the [ ] brush-size shortcut on
86    // AZERTY / QWERTZ.
87    if ((e.ctrlKey || e.metaKey) && !isAltGrEvent(e)) {
88      if (e.key === 'z') { e.preventDefault(); if (e.shiftKey) redo(); else undo(); }
89      // Ctrl+Shift+D = Deselect: clears the wand selection (and
90      // lasso if active) without affecting layers.
91      if (e.shiftKey && (e.key === 'D' || e.key === 'd')) {
92        if (state.wandMask || state.lassoPoints.length) {
93          e.preventDefault();
94          if (state.wandMask) {
95            saveState();
96            state.wandMask = null;
97            state.wandLayerId = null;
98            state.wandLastSeed = null;
99          }
100          if (state.lassoPoints.length) {
101            state.lassoPoints = [];
102            state.lassoActive = false;
103          }
104          composite();
105        }
106      }
107      // Save shortcuts — match the hints shown in the Save dropdown.
108      if ((e.key === 's' || e.key === 'S') && !e.altKey) {
109        e.preventDefault();
110        document.getElementById(e.shiftKey ? 'ge-export-gallery' : 'ge-save')?.click();
111      }
112      if (e.shiftKey && e.key === 'T') { e.preventDefault(); resizeCustomPrompt(); }
113      if (e.altKey && e.key === 't') { e.preventDefault(); startTransform(); }
114      // Ctrl+Alt+I — invert current selection. Uses e.code so
115      // Alt-modified key values (e.g. `ˆ` on Mac with Option+I)
116      // don't break the match.
117      if (e.altKey && e.code === 'KeyI') {
118        if (invertSelection()) {
119          e.preventDefault();
120          e.stopPropagation();
121        }
122      }
123      // Ctrl+Alt+J — new empty layer.
124      if (e.altKey && e.code === 'KeyJ') {
125        e.preventDefault();
126        e.stopPropagation();
127        addEmptyLayer();
128      }
129      // Wand selection: Delete = erase pixels. Ctrl+X = cut to
130      // clipboard + new layer + erase. Ctrl+C = copy.
131      // (Legacy `&& !_wandActive` clause referenced an undeclared
132      // variable — removed; the wand is selection-only and has no
133      // "active drag" state.)
134      if (state.wandMask) {
135        if (e.key === 'Delete' || e.key === 'Backspace') {
136          e.preventDefault();
137          wandDeleteSelection();
138          return;
139        }
140        if ((e.ctrlKey || e.metaKey) && (e.key === 'x' || e.key === 'c')) {
141          e.preventDefault();
142          const isCut = e.key === 'x';
143          const src = state.layers.find(l => l.id === state.wandLayerId);
144          if (!src) return;
145          // Clip source by wand mask into a temp canvas.
146          const w = src.canvas.width, h = src.canvas.height;
147          const tmp = document.createElement('canvas');
148          tmp.width = w; tmp.height = h;
149          const tCtx = tmp.getContext('2d');
150          tCtx.drawImage(src.canvas, 0, 0);
151          tCtx.globalCompositeOperation = 'destination-in';
152          tCtx.drawImage(state.wandMask, 0, 0);
153          state.internalClipboard = tmp;
154          tmp.toBlob(blob => {
155            if (blob && navigator.clipboard?.write) {
156              navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]).then(() => {
157                uiModule.showToast(isCut ? 'Cut to clipboard' : 'Copied to clipboard');
158              }).catch(() => uiModule.showToast(isCut ? 'Cut (editor only)' : 'Copied (editor only)'));
159            }
160          }, 'image/png');
161          if (isCut) {
162            // Cut also moves the selection to a new layer + erases source.
163            wandCopyToNewLayer();
164            wandDeleteSelection();
165          }
166          return;
167        }
168      }
169      if ((e.key === 'x' || e.key === 'c') && state.lassoPoints.length >= 3) {
170        e.preventDefault();
171        const layer = activeLayer();
172        if (!layer) return;
173        const off = state.layerOffsets.get(layer.id) || { x: 0, y: 0 };
174        const feather = parseInt(document.getElementById('ge-lasso-feather')?.value || '0');
175        const grow = parseInt(document.getElementById('ge-lasso-grow')?.value || '0');
176        const w = layer.canvas.width, h = layer.canvas.height;
177        const mask = buildLassoMask(w, h, off.x, off.y, feather, grow);
178        const srcData = layer.ctx.getImageData(0, 0, w, h);
179        const maskData = mask.getContext('2d').getImageData(0, 0, w, h);
180        // Build clipped image.
181        const tmp = document.createElement('canvas');
182        tmp.width = w; tmp.height = h;
183        const tCtx = tmp.getContext('2d');
184        const outData = tCtx.createImageData(w, h);
185        for (let i = 0; i < w * h; i++) {
186          const mv = maskData.data[i * 4] / 255;
187          if (mv > 0) {
188            outData.data[i*4] = srcData.data[i*4];
189            outData.data[i*4+1] = srcData.data[i*4+1];
190            outData.data[i*4+2] = srcData.data[i*4+2];
191            outData.data[i*4+3] = Math.round(srcData.data[i*4+3] * mv);
192          }
193        }
194        tCtx.putImageData(outData, 0, 0);
195        state.internalClipboard = tmp;
196        const isCut = e.key === 'x';
197        tmp.toBlob(blob => {
198          if (blob && navigator.clipboard?.write) {
199            navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]).then(() => {
200              uiModule.showToast(isCut ? 'Cut to clipboard' : 'Copied to clipboard');
201            }).catch(() =>
201 uiModule.showToast(isCut ? 'Cut (editor only)' : 'Copied (editor only)'));
202          }
203        }, 'image/png');
204        if (e.key === 'x') {
205          const savedPts = [...state.lassoPoints];
206          state.lassoPoints = savedPts;
207          lassoDeleteSelection();
208        } else {
209          state.lassoPoints = [];
210          composite();
211        }
212      }
213      // Ctrl+C with no active selection → copy the entire active layer
214      // to the system clipboard as a PNG. Gives a "just copy this image"
215      // shortcut without having to lasso-select-all first. The
216      // selection-aware Ctrl+C paths above run first (wand + lasso),
217      // so this only fires when neither is active.
218      if (e.key === 'c' && !e.shiftKey && !state.wandMask && state.lassoPoints.length < 3) {
219        const layer = activeLayer();
220        if (layer && layer.canvas && layer.canvas.width > 0) {
221          e.preventDefault();
222          layer.canvas.toBlob(blob => {
223            if (blob && navigator.clipboard?.write) {
224              navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })])
225                .then(() => uiModule.showToast('Layer copied to clipboard'))
226                .catch(() => uiModule.showToast('Copy failed (clipboard permission denied?)'));
227            }
228          }, 'image/png');
229          return;
230        }
231      }
232      // Ctrl+Alt+A = select all canvas.
233      if (e.altKey && e.key === 'a' && state.imgWidth > 0 && state.imgHeight > 0) {
234        e.preventDefault();
235        state.lassoPoints = [
236          { x: 0, y: 0 }, { x: state.imgWidth, y: 0 },
237          { x: state.imgWidth, y: state.imgHeight }, { x: 0, y: state.imgHeight },
238        ];
239        state.lassoActive = false;
240        composite();
241        drawLassoOverlay();
242        uiModule.showToast('All selected — Ctrl+C to copy, Del to delete');
243      }
244      // Ctrl+V handled by the paste event listener.
245      if (e.key === 'v') { /* no-op here */ }
246      return;
247    }
248    // Tool shortcuts (only when not typing in an input).
249    if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
250    const toolId = toolKeyMap[e.key.toLowerCase()];
251    if (toolId) {
252      const toolBtn = toolbar.querySelector(`[data-tool="${toolId}"]`);
253      if (toolBtn) toolBtn.click();
254    }
255    // Bracket keys for brush size — ±10% multiplier mirrors the
256    // exponential slider curve so each press feels the same at any
257    // size.
258    if (e.key === '[' || e.key === ']') {
259      const factor = e.key === '[' ? 0.9 : 1.1;
260      state.brushSize = Math.max(1, Math.min(800, Math.round(state.brushSize * factor)));
261      try { brushSizeSync(null); } catch {}
262    }
263    // Lasso shortcuts (when selection exists).
264    if (state.lassoPoints.length >= 3) {
265      if (e.key === 'Delete' || e.key === 'Backspace') { e.preventDefault(); lassoDeleteSelection(); }
266      if (e.key === 'd') { e.preventDefault(); lassoDeleteSelection(); }
267      if (e.key === 'c') { e.preventDefault(); lassoCopyToLayer(); }
268      if (e.key === 'm') { e.preventDefault(); lassoToMask(); }
269    }
270  });
271}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.