Add start of recording new keymap entries (they don't get saved when submitted yet)

This commit is contained in:
Frank Noirot
2024-05-28 19:36:41 -04:00
parent c140a038c8
commit cd554b7f93
6 changed files with 153 additions and 54 deletions

View File

@ -10,6 +10,7 @@ import { useInteractionMap } from 'hooks/useInteractionMap'
import { ActionButtonDropdown } from 'components/ActionButtonDropdown'
import { useHotkeys } from 'react-hotkeys-hook'
import Tooltip from 'components/Tooltip'
import { KEYBINDING_CATEGORIES } from 'lib/constants'
export function Toolbar({
className = '',
@ -40,7 +41,8 @@ export function Toolbar({
guard: () => !shouldDisableModelingActions && state.matches('idle'),
},
],
[shouldDisableModelingActions, commandBarSend, state]
[shouldDisableModelingActions, commandBarSend, state],
KEYBINDING_CATEGORIES.MODELING
)
const iconClassName =
'group-disabled:text-chalkboard-50 group-enabled:group-hover:!text-primary dark:group-enabled:group-hover:!text-inherit group-pressed:!text-chalkboard-10 group-ui-open:!text-chalkboard-10 dark:group-ui-open:!text-chalkboard-10'

View File

@ -5,9 +5,8 @@ import CommandBarArgument from './CommandBarArgument'
import CommandComboBox from '../CommandComboBox'
import CommandBarReview from './CommandBarReview'
import { useLocation } from 'react-router-dom'
import { InteractionMapItem } from 'machines/interactionMapMachine'
import { useInteractionMap } from 'hooks/useInteractionMap'
import useHotkeyWrapper from 'lib/hotkeyWrapper'
import { KEYBINDING_CATEGORIES } from 'lib/constants'
export const CommandBar = () => {
const { pathname } = useLocation()
@ -49,7 +48,7 @@ export const CommandBar = () => {
},
],
[commandBarState, commandBarSend],
'Command Bar'
KEYBINDING_CATEGORIES.COMMAND_BAR
)
function stepBack() {

View File

@ -1,6 +1,12 @@
import { useMachine } from '@xstate/react'
import { INTERACTION_MAP_SEPARATOR } from 'lib/constants'
import { isModifierKey, mapKey, sortKeys } from 'lib/keyboard'
import {
isModifierKey,
mapKey,
mouseButtonToName,
resolveInteractionEvent,
sortKeys,
} from 'lib/keyboard'
import {
MouseButtonName,
interactionMapMachine,
@ -91,33 +97,18 @@ export function InteractionMapMachineProvider({
},
services: {
'Resolve hotkey by prefix': (context, event) => {
// First determine if we have a mouse or keyboard event
const action =
'key' in event.data
? mapKey(event.data.code)
: mouseButtonToName(event.data.button)
console.log('action', action)
const resolvedInteraction = resolveInteractionEvent(event.data)
// if the key is already a modifier key, skip everything else and reject
if (isModifierKey(action)) {
if (resolvedInteraction.isModifier) {
// We return an empty string so that we don't clear the currentSequence
return Promise.reject('')
}
const modifiers = [
event.data.ctrlKey && 'ctrl',
event.data.shiftKey && 'shift',
event.data.altKey && 'alt',
event.data.metaKey && 'meta',
].filter((item) => item !== false) as string[]
const step = [action, ...modifiers]
.sort(sortKeys)
.join(INTERACTION_MAP_SEPARATOR)
// Find all the sequences that start with the current sequence
const searchString =
(context.currentSequence ? context.currentSequence + ' ' : '') + step
(context.currentSequence ? context.currentSequence + ' ' : '') +
resolvedInteraction.asString
const matches = context.interactionMap.filter((item) =>
item.sequence.startsWith(searchString)
@ -141,7 +132,7 @@ export function InteractionMapMachineProvider({
// We have a prefix match.
// Reject the promise and return the step
// so we can add it to currentSequence
return Promise.reject(step)
return Promise.reject(resolvedInteraction.asString)
}
// Resolve to just one exact match
@ -211,16 +202,3 @@ export function InteractionMapMachineProvider({
</InteractionMapMachineContext.Provider>
)
}
function mouseButtonToName(button: MouseEvent['button']): MouseButtonName {
switch (button) {
case 0:
return 'LeftButton'
case 1:
return 'MiddleButton'
case 2:
return 'RightButton'
default:
return 'LeftButton'
}
}

View File

@ -1,4 +1,14 @@
import { ActionIcon } from 'components/ActionIcon'
import { useInteractionMapContext } from 'hooks/useInteractionMapContext'
import {
isModifierKey,
mapKey,
mouseButtonToName,
resolveInteractionEvent,
} from 'lib/keyboard'
import { InteractionMapItem } from 'machines/interactionMapMachine'
import { useEffect, useState } from 'react'
import { g } from 'vitest/dist/suite-IbNSsUWN'
export function AllKeybindingsFields() {
const { state } = useInteractionMapContext()
@ -6,24 +16,81 @@ export function AllKeybindingsFields() {
<div className="relative overflow-y-auto">
<div className="flex flex-col gap-4 px-2">
{state.context.interactionMap.map((item) => (
<div
key={item.ownerId + '-' + item.name}
className="flex gap-2 justify-between"
>
<h3>{item.title}</h3>
<div className="flex gap-3">
{item.sequence.split(' ').map((chord) => (
<kbd
key={`${item.ownerId}-${item.name}-${chord}`}
className="py-0.5 px-1.5 rounded bg-primary/10 dark:bg-chalkboard-80"
>
{chord}
</kbd>
))}
</div>
</div>
<KeybindingField key={item.ownerId + '-' + item.name} item={item} />
))}
</div>
</div>
)
}
function KeybindingField({ item }: { item: InteractionMapItem }) {
const [isEditing, setIsEditing] = useState(false)
const [newSequence, setNewSequence] = useState('')
useEffect(() => {
const blockOtherEvents = (e: KeyboardEvent | MouseEvent) => {
e.preventDefault()
e.stopPropagation()
e.stopImmediatePropagation()
}
const handleInteraction = (e: KeyboardEvent | MouseEvent) => {
blockOtherEvents(e)
const resolvedInteraction = resolveInteractionEvent(e)
if (resolvedInteraction.isModifier) return
setNewSequence(
(prev) => prev + (prev.length ? ' ' : '') + resolvedInteraction.asString
)
}
const handleContextMenu = (e: MouseEvent) => {
blockOtherEvents(e)
}
if (!isEditing) {
setNewSequence('')
globalThis?.window?.removeEventListener('keydown', handleInteraction)
globalThis?.window?.removeEventListener('mousedown', handleInteraction)
globalThis?.window?.removeEventListener('contextmenu', handleContextMenu)
} else {
globalThis?.window?.addEventListener('keydown', handleInteraction)
globalThis?.window?.addEventListener('mousedown', handleInteraction)
globalThis?.window?.addEventListener('contextmenu', handleContextMenu)
}
return () => {
globalThis?.window?.removeEventListener('keydown', handleInteraction)
globalThis?.window?.removeEventListener('mousedown', handleInteraction)
globalThis?.window?.removeEventListener('contextmenu', handleContextMenu)
}
}, [isEditing])
return (
<div
key={item.ownerId + '-' + item.name}
className="flex gap-2 justify-between items-start"
>
<h3>{item.title}</h3>
<div className="flex-1 flex flex-wrap justify-end gap-3">
{(isEditing ? newSequence : item.sequence)
.split(' ')
.map((chord, i) => (
<kbd
key={`${item.ownerId}-${item.name}-${chord}-${i}`}
className="py-0.5 px-1.5 rounded bg-primary/10 dark:bg-chalkboard-80"
>
{chord}
</kbd>
))}
</div>
<button
onClick={() => setIsEditing((prev) => !prev)}
className="p-0 m-0"
type={isEditing ? 'submit' : 'button'}
>
<ActionIcon icon={isEditing ? 'checkmark' : 'sketch'} />
</button>
</div>
)
}

View File

@ -48,3 +48,8 @@ export const ONBOARDING_PROJECT_NAME = 'Tutorial Project $nn'
export const INTERACTION_MAP_SEPARATOR = '+'
/** The default KCL length expression */
export const KCL_DEFAULT_LENGTH = `5`
/** keybinding categories */
export const KEYBINDING_CATEGORIES = {
MODELING: 'modeling',
COMMAND_BAR: 'command-bar',
}

View File

@ -1,3 +1,6 @@
import { MouseButtonName } from 'machines/interactionMapMachine'
import { INTERACTION_MAP_SEPARATOR } from './constants'
/**
* From https://github.com/JohannesKlauss/react-hotkeys-hook/blob/main/src/parseHotkeys.ts
* we don't want to use the whole library (as cool as it is) because it attaches
@ -43,3 +46,48 @@ export function isModifierKey(key: string) {
export function sortKeys(a: string, b: string) {
return isModifierKey(a) ? -1 : isModifierKey(b) ? 1 : a.localeCompare(b)
}
export function mouseButtonToName(
button: MouseEvent['button']
): MouseButtonName {
switch (button) {
case 0:
return 'LeftButton'
case 1:
return 'MiddleButton'
case 2:
return 'RightButton'
default:
return 'LeftButton'
}
}
type ResolveKeymapEvent = {
action: string
modifiers: string[]
isModifier: boolean
asString: string
}
export function resolveInteractionEvent(
event: MouseEvent | KeyboardEvent
): ResolveKeymapEvent {
// First, determine if this is a key or mouse event
const action =
'key' in event ? mapKey(event.code) : mouseButtonToName(event.button)
const modifiers = [
event.ctrlKey && 'ctrl',
event.shiftKey && 'shift',
event.altKey && 'alt',
event.metaKey && 'meta',
].filter((item) => item !== false) as string[]
return {
action,
modifiers,
isModifier: isModifierKey(action),
asString: [action, ...modifiers]
.sort(sortKeys)
.join(INTERACTION_MAP_SEPARATOR),
}
}