diff --git a/src/simulator/src/hotkey_binder/model/actions.js b/src/simulator/src/hotkey_binder/model/actions.js deleted file mode 100644 index 113f37cc..00000000 --- a/src/simulator/src/hotkey_binder/model/actions.js +++ /dev/null @@ -1,210 +0,0 @@ -import { defaultKeys } from '../defaultKeys' -import { addShortcut } from './addShortcut' -import { updateHTML } from '../view/panel.ui' -import { simulationArea } from '../../simulationArea' -import { - scheduleUpdate, - wireToBeCheckedSet, - updateCanvasSet, -} from '../../engine' - -import { getOS } from './utils.js' -import { shortcut } from './shortcuts.plugin.js' -/** - * Function used to add or change keys user or default - * grabs the keycombo from localstorage & - * calls the addShortcut function in a loop to bind them - * @param {string} mode - user custom keys or default keys - */ -export const addKeys = (mode) => { - shortcut.removeAll() - if (mode === 'user') { - localStorage.removeItem('defaultKeys') - let userKeys = localStorage.get('userKeys') - for (let pref in userKeys) { - let key = userKeys[pref] - key = key.split(' ').join('') - addShortcut(key, pref) - } - updateHTML('user') - } else if (mode == 'default') { - if (localStorage.userKeys) localStorage.removeItem('userKeys') - let defaultKeys = localStorage.get('defaultKeys') - for (let pref in defaultKeys) { - let key = defaultKeys[pref] - key = key.split(' ').join('') - addShortcut(key, pref) - } - updateHTML('default') - } -} -/** - * Function used to check if new keys are added, adds missing keys if added - */ -export const checkUpdate = () => { - const userK = localStorage.get('userKeys') - if (Object.size(userK) !== Object.size(defaultKeys)) { - for (const [key, value] of Object.entries(defaultKeys)) { - if (!Object.keys(userK).includes(key)) { - userK[key] = value - } - } - localStorage.set('userKeys', userK) - } else { - return - } -} -/** - * Function used to set userKeys, grabs the keycombo from the panel UI - * sets it to the localStorage & cals addKeys - * removes the defaultkeys from localStorage - */ -export const setUserKeys = () => { - if (localStorage.defaultKeys) localStorage.removeItem('defaultKeys') - let userKeys = {} - let x = 0 - const preferenceChildren = document.getElementById('preference').children; - - while (preferenceChildren[x]) { - const keyElement = preferenceChildren[x].children[1].children[0]; - const valueElement = preferenceChildren[x].children[1].children[1]; - - userKeys[keyElement.innerText] = valueElement.innerText; - x++ - } - localStorage.set('userKeys', userKeys) - addKeys('user') -} -/** - * Function used to set defaultKeys, grabs the keycombo from the defaultkeys metadata - * sets it to the localStorage & cals addKeys - * removes the userkeys from localStorage if present - * also checks for OS type - */ -export const setDefault = () => { - if (localStorage.userKeys) localStorage.removeItem('userKeys') - if (getOS() === 'MacOS') { - const macDefaultKeys = {} - for (let [key, value] of Object.entries(defaultKeys)) { - if (value.split(' + ')[0] == 'Ctrl'); - macDefaultKeys[key] = - value.split(' + ')[0] == 'Ctrl' - ? value.replace('Ctrl', 'Meta') - : value - localStorage.set('defaultKeys', macDefaultKeys) - } - } else { - localStorage.set('defaultKeys', defaultKeys) //TODO add a confirmation alert - } - addKeys('default') -} -/** - * function to check if user entered keys are already assigned to other key - * gives a warning message if keys already assigned - * @param {string} combo the key combo - * @param {string} target the target option of the panel - */ -export const warnOverride = (combo, target, warning) => { - let x = 0 - const preferenceChildren = document.getElementById('preference').children; - - while (preferenceChildren[x]) { - const element = preferenceChildren[x].children[1].children[1]; - const assignee = preferenceChildren[x].children[1].children[0].innerText; - // $('#warning').text( - // `This key(s) is already assigned to: ${assignee}, press Enter to override.` - // ) - if (element.innerText === combo && assignee !== target.previousElementSibling.innerText) { - warning.value = `This key(s) is already assigned to: ${assignee}, press Enter to override.`; - document.getElementById('edit').style.border = '1.5px solid #dc5656'; - return - } else { - document.getElementById('edit').style.border = 'none'; - } - x++ - } -} - -export const elementDirection = (direct) => () => { - if (simulationArea.lastSelected) { - simulationArea.lastSelected.newDirection(direct.toUpperCase()) - - const selectElement = document.querySelector("select[name^='newDirection']"); - if (selectElement) { - selectElement.value = direct.toUpperCase(); - } - - updateSystem() - } -} - -export const labelDirection = (direct) => () => { - if (simulationArea.lastSelected && !simulationArea.lastSelected.labelDirectionFixed) { - simulationArea.lastSelected.labelDirection = direct.toUpperCase(); - document.querySelector("select[name^='newLabelDirection']").value = direct.toUpperCase(); - updateSystem() - } -} - -export const insertLabel = () => { - if (simulationArea.lastSelected) { - document.querySelector("input[name^='setLabel']").focus(); - if (!document.querySelector("input[name^='setLabel']").value) { - document.querySelector("input[name^='setLabel']").value = 'Untitled'; - } - document.querySelector("input[name^='setLabel']").select(); - updateSystem() - } -} - -export const moveElement = (direct) => () => { - if (simulationArea.lastSelected) { - switch (direct) { - case 'up': - simulationArea.lastSelected.y -= 10 - break - case 'down': - simulationArea.lastSelected.y += 10 - break - case 'left': - simulationArea.lastSelected.x -= 10 - break - case 'right': - simulationArea.lastSelected.x += 10 - break - } - updateSystem() - } -} - -export const openHotkey = () => { - const customShortcutElement = document.getElementById('customShortcut'); - if (customShortcutElement) { - customShortcutElement.click(); - } -} - -// export const createNewCircuitScopeCall = () => { -// const createNewCircuitScopeElement = document.getElementById('createNewCircuitScope'); // TODO: remove later -// if (createNewCircuitScopeElement) { -// createNewCircuitScopeElement.click(); -// } -// } - -export const openDocumentation = () => { - if ( - simulationArea.lastSelected == undefined || - simulationArea.lastSelected.helplink == undefined - ) { - // didn't select any element or documentation not found - window.open('https://docs.circuitverse.org/', '_blank') - } else { - window.open(simulationArea.lastSelected.helplink, '_blank') - } -} - -function updateSystem() { - updateCanvasSet(true) - wireToBeCheckedSet(1) - scheduleUpdate(1) -} diff --git a/src/simulator/src/hotkey_binder/model/actions.ts b/src/simulator/src/hotkey_binder/model/actions.ts new file mode 100644 index 00000000..33452764 --- /dev/null +++ b/src/simulator/src/hotkey_binder/model/actions.ts @@ -0,0 +1,340 @@ +/* eslint-disable import/no-cycle */ +import { defaultKeys } from '../defaultKeys' +import { addShortcut } from './addShortcut' +import { updateHTML } from '../view/panel.ui' +import { simulationArea } from '../../simulationArea' +import { + scheduleUpdate, + wireToBeCheckedSet, + updateCanvasSet, +} from '../../engine' + +import { getOS } from './utils' +import { shortcut } from './shortcuts.plugin' + +import { KeyMap } from './model.types' + +type DirectionType = 'up' | 'down' | 'left' | 'right' + +/** + * Function used to add or change keys user or default + * grabs the keycombo from localstorage & + * calls the addShortcut function in a loop to bind them + * @param {string} mode - user custom keys or default keys + */ +export const addKeys = (mode: 'user' | 'default'): void => { + shortcut.removeAll() + const keys = mode === 'user' ? getUserKeys() : getDefaultKeys() + bindKeys(keys, mode) +} + +/** + * Get user keys from localStorage + */ +const getUserKeys = (): KeyMap => { + localStorage.removeItem('defaultKeys') + return JSON.parse(localStorage.getItem('userKeys') || '{}') +} + +/** + * Get default keys from localStorage + */ +const getDefaultKeys = (): KeyMap => { + if (localStorage.userKeys) localStorage.removeItem('userKeys') + return JSON.parse(localStorage.getItem('defaultKeys') || '{}') +} + +/** + * Bind keys to shortcuts + */ +const bindKeys = (keys: KeyMap, mode: 'user' | 'default'): void => { + Object.entries(keys).forEach(([pref, key]) => { + const normalizedKey = key.split(' ').join('') + addShortcut(normalizedKey, pref) + }) + updateHTML(mode) +} + +/** + * Function used to check if new keys are added, adds missing keys if added + */ +export const checkUpdate = (): void => { + const userK: KeyMap = JSON.parse(localStorage.getItem('userKeys') || '{}'); + const defaultK: KeyMap = defaultKeys; + + const hasChanges = syncKeys(userK, defaultK); + + if (hasChanges) { + localStorage.setItem('userKeys', JSON.stringify(userK)); + } +}; + +const syncKeys = (userK: KeyMap, defaultK: KeyMap): boolean => { + const hasAddedOrUpdated = addOrUpdateKeys(userK, defaultK); + const hasRemoved = removeObsoleteKeys(userK, defaultK); + + return hasAddedOrUpdated || hasRemoved; +}; + +const addOrUpdateKeys = (userK: KeyMap, defaultK: KeyMap): boolean => { + let hasChanges = false; + + for (const key of Object.keys(defaultK)) { + if (!Object.hasOwn(userK, key) || userK[key] !== defaultK[key]) { + userK[key] = defaultK[key]; + hasChanges = true; + } + } + + return hasChanges; +}; + +const removeObsoleteKeys = (userK: KeyMap, defaultK: KeyMap): boolean => { + let hasChanges = false; + + for (const key of Object.keys(userK)) { + if (!Object.hasOwn(defaultK, key)) { + delete userK[key]; + hasChanges = true; + } + } + + return hasChanges; +}; + +/** + * Add missing keys to user keys + */ +const addMissingKeys = (userK: KeyMap): void => { + Object.entries(defaultKeys).forEach(([key, value]) => { + if (!userK[key]) { + userK[key] = value + } + }) +} + +/** + * Function used to set userKeys, grabs the keycombo from the panel UI + * sets it to the localStorage & calls addKeys + * removes the defaultkeys from localStorage + */ +export const setUserKeys = (): void => { + if (localStorage.defaultKeys) localStorage.removeItem('defaultKeys') + const userKeys = getUserKeysFromUI() + localStorage.setItem('userKeys', JSON.stringify(userKeys)) + addKeys('user') +} + +/** + * Get user keys from the UI + */ +const getUserKeysFromUI = (): KeyMap => { + const userKeys: KeyMap = {} + const preferenceChildren = document.getElementById('preference')?.children + if (!preferenceChildren) return userKeys + + Array.from(preferenceChildren).forEach((child) => { + const keyChild = child?.children[1]?.children[0] + const valueChild = child?.children[1]?.children[1] + + if (keyChild instanceof HTMLElement && valueChild instanceof HTMLElement) { + userKeys[keyChild.innerText] = valueChild.innerText + } + }) + return userKeys +} + +/** + * Function used to set defaultKeys, grabs the keycombo from the defaultkeys metadata + * sets it to the localStorage & calls addKeys + * removes the userkeys from localStorage if present + * also checks for OS type + */ +export const setDefault = (): void => { + if (localStorage.getItem('userKeys')) localStorage.removeItem('userKeys') + const keys = getOS() === 'MacOS' ? getMacDefaultKeys() : defaultKeys + localStorage.setItem('defaultKeys', JSON.stringify(keys)) + addKeys('default') +} + +/** + * Get default keys for MacOS + */ +const getMacDefaultKeys = (): KeyMap => { + const macDefaultKeys: KeyMap = {} + Object.entries(defaultKeys).forEach(([key, value]) => { + macDefaultKeys[key] = value.split(' + ')[0] === 'Ctrl' ? value.replace('Ctrl', 'Meta') : value + }) + return macDefaultKeys +} + +/** + * Function to check if user entered keys are already assigned to other key + * gives a warning message if keys already assigned + */ +export const warnOverride = ( + combo: string, + target: HTMLElement, + warning: HTMLInputElement +): void => { + const preferenceChildren = document.getElementById('preference')?.children + if (!preferenceChildren) return + + const isComboAssigned = checkIfComboIsAssigned(combo, target, preferenceChildren) + if (isComboAssigned) { + warning.value = `This key(s) is already assigned to: ${isComboAssigned}, press Enter to override.` + setEditElementBorder('#dc5656') + } else { + setEditElementBorder('none') + } +} + +/** + * Check if the key combo is already assigned to another key + */ +const checkIfComboIsAssigned = ( + combo: string, + target: HTMLElement, + preferenceChildren: HTMLCollection +): string | undefined => { + return Array.from(preferenceChildren).reduce((acc, child) => { + if (acc) return acc + return getAssigneeFromPreference(child, combo, target) + }, undefined) +} + +/** + * Get the assignee from a preference element if the combo matches + */ +const getAssigneeFromPreference = ( + preferenceElement: Element | null, + combo: string, + target: HTMLElement +): string | undefined => { + const keyChild = preferenceElement?.children[1]?.children[0] + const valueChild = preferenceElement?.children[1]?.children[1] + + if (keyChild instanceof HTMLElement && valueChild instanceof HTMLElement) { + const assignee = keyChild.innerText + if (valueChild.innerText === combo && + assignee !== (target.previousElementSibling as HTMLElement)?.innerText) { + return assignee + } + } + return undefined +} + +/** + * Set border style for edit element + */ +const setEditElementBorder = (color: string): void => { + const editElement = document.getElementById('edit') + if (editElement) { + editElement.style.border = color === 'none' ? 'none' : `1.5px solid ${color}` + } +} + +/** + * Update element direction + */ +export const elementDirection = (direct: string) => (): void => { + if (simulationArea.lastSelected) { + simulationArea.lastSelected.newDirection(direct.toUpperCase()) + updateSelectElement("select[name^='newDirection']", direct.toUpperCase()) + updateSystem() + } +} + +/** + * Update label direction + */ +export const labelDirection = (direct: string) => (): void => { + if (simulationArea.lastSelected && !simulationArea.lastSelected.labelDirectionFixed) { + simulationArea.lastSelected.labelDirection = direct.toUpperCase() + updateSelectElement("select[name^='newLabelDirection']", direct.toUpperCase()) + updateSystem() + } +} + +/** + * Update select element value + */ +const updateSelectElement = (selector: string, value: string): void => { + const selectElement = document.querySelector(selector) + if (selectElement) { + selectElement.value = value + } +} + +/** + * Insert label into input field + */ +export const insertLabel = (): void => { + if (!simulationArea.lastSelected) return + + const labelInput = document.querySelector("input[name^='setLabel']") + if (!labelInput) return + + focusAndSetLabel(labelInput) + updateSystem() +} + +/** + * Focus on the label input and set a default value if empty + */ +const focusAndSetLabel = (labelInput: HTMLInputElement): void => { + labelInput.focus() + if (!labelInput.value) { + labelInput.value = 'Untitled' + } + labelInput.select() +} + +/** + * Move element in a specific direction + */ +export const moveElement = (direct: DirectionType) => (): void => { + if (simulationArea.lastSelected) { + const { x, y } = simulationArea.lastSelected + const newPosition = calculateNewPosition(direct, x, y) + simulationArea.lastSelected.x = newPosition.x + simulationArea.lastSelected.y = newPosition.y + updateSystem() + } +} + +/** + * Calculate new position based on direction + */ +const calculateNewPosition = (direct: DirectionType, x: number, y: number): { x: number; y: number } => { + switch (direct) { + case 'up': return { x, y: y - 10 } + case 'down': return { x, y: y + 10 } + case 'left': return { x: x - 10, y } + case 'right': return { x: x + 10, y } + } +} + +/** + * Open hotkey settings + */ +export const openHotkey = (): void => { + document.getElementById('customShortcut')?.click() +} + +/** + * Open documentation + */ +export const openDocumentation = (): void => { + const url = simulationArea.lastSelected?.helplink || 'https://docs.circuitverse.org/' + window.open(url, '_blank') +} + +/** + * Update system state + */ +function updateSystem(): void { + updateCanvasSet(true) + wireToBeCheckedSet(1) + scheduleUpdate(1) +} \ No newline at end of file