Add start of recording new keymap entries (they don't get saved when submitted yet)
This commit is contained in:
@ -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'
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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',
|
||||
}
|
||||
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user