| export type ShortcutCategory = 'Panels' | 'Canvas' | 'Editing' | 'View' | 'Window'; |
| export type ShortcutId = |
| | 'edit.undo' | 'edit.redo' | 'file.save' | 'capture.screen' |
| | 'view.focus' | 'view.valueMirror' | 'view.zoomLens' | 'view.fitAll' | 'view.zoom100' |
| | 'selection.duplicate' | 'selection.group' | 'selection.ungroup' | 'selection.delete' | 'selection.selectAll' | 'selection.flipH' | 'selection.desaturate' |
| | 'panel.browser' | 'panel.library' | 'panel.settings' | 'panel.closeAll' |
| | 'tool.annotate' | 'tool.globalDesaturate' | 'window.alwaysOnTop'; |
|
|
| export type ShortcutCombo = { primary?: boolean; ctrl?: boolean; meta?: boolean; alt?: boolean; shift?: boolean; code: string; displayKey?: string }; |
| export type ShortcutDef = { id: ShortcutId; label: string; category: ShortcutCategory; description?: string; defaultCombos: ShortcutCombo[]; allowInEditable?: boolean; preventDefault?: boolean; singleKey?: boolean }; |
| export type ShortcutSettings = { version: 1; shortcuts: Partial<Record<ShortcutId, ShortcutCombo[] | null>> }; |
|
|
| export const DEFAULT_SHORTCUTS: ShortcutDef[] = [ |
| { id:'edit.undo', label:'Undo', category:'Editing', defaultCombos:[{primary:true, code:'KeyZ', displayKey:'Z'}] }, |
| { id:'edit.redo', label:'Redo', category:'Editing', defaultCombos:[{primary:true, shift:true, code:'KeyZ', displayKey:'Z'}] }, |
| { id:'file.save', label:'Save board', category:'Editing', defaultCombos:[{primary:true, code:'KeyS', displayKey:'S'}] }, |
| { id:'capture.screen', label:'Screen capture', category:'Canvas', defaultCombos:[{shift:true, code:'KeyS', displayKey:'S'}] }, |
| { id:'view.focus', label:'Focus selected image', category:'View', defaultCombos:[{code:'KeyF', displayKey:'F'}], singleKey:true }, |
| { id:'view.valueMirror', label:'Value mirror split', category:'View', defaultCombos:[{code:'KeyV', displayKey:'V'}], singleKey:true }, |
| { id:'view.zoomLens', label:'Temporary zoom lens', category:'View', defaultCombos:[{code:'KeyZ', displayKey:'Z'}], singleKey:true }, |
| { id:'view.fitAll', label:'Fit all images', category:'View', defaultCombos:[{primary:true, code:'Digit0', displayKey:'0'}] }, |
| { id:'view.zoom100', label:'Zoom to 100%', category:'View', defaultCombos:[{primary:true, code:'Digit1', displayKey:'1'}] }, |
| { id:'selection.duplicate', label:'Duplicate selection', category:'Editing', defaultCombos:[{primary:true, code:'KeyD', displayKey:'D'}] }, |
| { id:'selection.group', label:'Group selection', category:'Editing', defaultCombos:[{primary:true, code:'KeyG', displayKey:'G'}] }, |
| { id:'selection.ungroup', label:'Ungroup selection', category:'Editing', defaultCombos:[{primary:true, shift:true, code:'KeyG', displayKey:'G'}] }, |
| { id:'selection.delete', label:'Delete selection', category:'Editing', defaultCombos:[{code:'Delete', displayKey:'Delete'}, {code:'Backspace', displayKey:'Backspace'}] }, |
| { id:'selection.selectAll', label:'Select all images', category:'Editing', defaultCombos:[{primary:true, code:'KeyA', displayKey:'A'}] }, |
| { id:'selection.flipH', label:'Flip selection horizontally', category:'Editing', defaultCombos:[{code:'KeyH', displayKey:'H'}], singleKey:true }, |
| { id:'selection.desaturate', label:'Desaturate selection', category:'Canvas', defaultCombos:[{code:'KeyD', displayKey:'D'}], singleKey:true }, |
| { id:'panel.browser', label:'Toggle browser panel', category:'Panels', defaultCombos:[{code:'KeyB', displayKey:'B'}], singleKey:true }, |
| { id:'panel.library', label:'Toggle library panel', category:'Panels', defaultCombos:[{code:'KeyL', displayKey:'L'}], singleKey:true }, |
| { id:'panel.settings', label:'Open settings', category:'Panels', defaultCombos:[{primary:true, code:'Comma', displayKey:','}] }, |
| { id:'panel.closeAll', label:'Close panels / clear selection', category:'Panels', defaultCombos:[{code:'Escape', displayKey:'Esc'}] }, |
| { id:'tool.annotate', label:'Toggle annotation mode', category:'Canvas', defaultCombos:[{code:'KeyA', displayKey:'A'}], singleKey:true }, |
| { id:'tool.globalDesaturate', label:'Desaturate all images', category:'Canvas', defaultCombos:[{shift:true, code:'KeyD', displayKey:'D'}] }, |
| { id:'window.alwaysOnTop', label:'Toggle always on top', category:'Window', defaultCombos:[{code:'KeyT', displayKey:'T'}], singleKey:true }, |
| ]; |
|
|
| const STORAGE_KEY = 'refstudio.shortcuts.v1'; |
| const MODIFIER_CODES = new Set(['ShiftLeft','ShiftRight','ControlLeft','ControlRight','AltLeft','AltRight','MetaLeft','MetaRight']); |
| export const isMac = () => navigator.platform.toLowerCase().includes('mac'); |
|
|
| export function normalizeEventToCombo(e: KeyboardEvent): ShortcutCombo | null { |
| if (e.isComposing || e.key === 'Dead' || e.getModifierState?.('AltGraph') || MODIFIER_CODES.has(e.code)) return null; |
| const mac = isMac(); |
| return { primary: mac ? e.metaKey || undefined : e.ctrlKey || undefined, ctrl: mac && e.ctrlKey || undefined, meta: !mac && e.metaKey || undefined, alt: e.altKey || undefined, shift: e.shiftKey || undefined, code: e.code, displayKey: e.code === 'Space' ? 'Space' : e.key.length === 1 ? e.key.toUpperCase() : e.key }; |
| } |
| export function comboToCanonical(c: ShortcutCombo): string { return [c.primary?'Primary':'', c.ctrl?'Ctrl':'', c.meta?'Meta':'', c.alt?'Alt':'', c.shift?'Shift':'', c.code].filter(Boolean).join('+'); } |
| export function comboToDisplay(c: ShortcutCombo): string { const mac=isMac(); const parts:string[]=[]; if(c.primary)parts.push(mac?'⌘':'Ctrl'); if(c.ctrl)parts.push('Ctrl'); if(c.meta)parts.push(mac?'⌘':'Meta'); if(c.alt)parts.push(mac?'⌥':'Alt'); if(c.shift)parts.push(mac?'⇧':'Shift'); parts.push(c.displayKey || c.code.replace(/^Key/,'').replace(/^Digit/,'')); return mac?parts.join(''):parts.join('+'); } |
| export function loadShortcutSettings(): ShortcutSettings { try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '') || {version:1, shortcuts:{}}; } catch { return {version:1, shortcuts:{}}; } } |
| export function saveShortcutSettings(s: ShortcutSettings) { localStorage.setItem(STORAGE_KEY, JSON.stringify(s)); window.dispatchEvent(new CustomEvent('refstudio:shortcuts-updated')); } |
| export function effectiveCombos(def: ShortcutDef, settings = loadShortcutSettings()): ShortcutCombo[] { const override = settings.shortcuts[def.id]; if (override === null || override === undefined) return def.defaultCombos; return override; } |
| export function matchesShortcut(id: ShortcutId, e: KeyboardEvent, settings = loadShortcutSettings()): boolean { const combo = normalizeEventToCombo(e); if(!combo) return false; const def = DEFAULT_SHORTCUTS.find(d=>d.id===id); if(!def) return false; const key = comboToCanonical(combo); return effectiveCombos(def, settings).some(c=>comboToCanonical(c)===key); } |
| export function isEditableTarget(t: EventTarget | null) { const el = t as HTMLElement | null; return !!el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT' || el.isContentEditable); } |
| export function findConflicts(settings = loadShortcutSettings()) { const map = new Map<string, ShortcutDef[]>(); for(const def of DEFAULT_SHORTCUTS){ for(const combo of effectiveCombos(def, settings)){ const k = comboToCanonical(combo); const arr = map.get(k) || []; arr.push(def); map.set(k, arr); } } return [...map.entries()].filter(([,defs])=>defs.length>1).map(([combo, actions])=>({combo, actions})); } |
|
|