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 { ActionButtonDropdown } from 'components/ActionButtonDropdown'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import Tooltip from 'components/Tooltip'
|
import Tooltip from 'components/Tooltip'
|
||||||
|
import { KEYBINDING_CATEGORIES } from 'lib/constants'
|
||||||
|
|
||||||
export function Toolbar({
|
export function Toolbar({
|
||||||
className = '',
|
className = '',
|
||||||
@ -40,7 +41,8 @@ export function Toolbar({
|
|||||||
guard: () => !shouldDisableModelingActions && state.matches('idle'),
|
guard: () => !shouldDisableModelingActions && state.matches('idle'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[shouldDisableModelingActions, commandBarSend, state]
|
[shouldDisableModelingActions, commandBarSend, state],
|
||||||
|
KEYBINDING_CATEGORIES.MODELING
|
||||||
)
|
)
|
||||||
const iconClassName =
|
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'
|
'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 CommandComboBox from '../CommandComboBox'
|
||||||
import CommandBarReview from './CommandBarReview'
|
import CommandBarReview from './CommandBarReview'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import { InteractionMapItem } from 'machines/interactionMapMachine'
|
|
||||||
import { useInteractionMap } from 'hooks/useInteractionMap'
|
import { useInteractionMap } from 'hooks/useInteractionMap'
|
||||||
import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
import { KEYBINDING_CATEGORIES } from 'lib/constants'
|
||||||
|
|
||||||
export const CommandBar = () => {
|
export const CommandBar = () => {
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
@ -49,7 +48,7 @@ export const CommandBar = () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
[commandBarState, commandBarSend],
|
[commandBarState, commandBarSend],
|
||||||
'Command Bar'
|
KEYBINDING_CATEGORIES.COMMAND_BAR
|
||||||
)
|
)
|
||||||
|
|
||||||
function stepBack() {
|
function stepBack() {
|
||||||
|
|||||||
@ -1,6 +1,12 @@
|
|||||||
import { useMachine } from '@xstate/react'
|
import { useMachine } from '@xstate/react'
|
||||||
import { INTERACTION_MAP_SEPARATOR } from 'lib/constants'
|
import { INTERACTION_MAP_SEPARATOR } from 'lib/constants'
|
||||||
import { isModifierKey, mapKey, sortKeys } from 'lib/keyboard'
|
import {
|
||||||
|
isModifierKey,
|
||||||
|
mapKey,
|
||||||
|
mouseButtonToName,
|
||||||
|
resolveInteractionEvent,
|
||||||
|
sortKeys,
|
||||||
|
} from 'lib/keyboard'
|
||||||
import {
|
import {
|
||||||
MouseButtonName,
|
MouseButtonName,
|
||||||
interactionMapMachine,
|
interactionMapMachine,
|
||||||
@ -91,33 +97,18 @@ export function InteractionMapMachineProvider({
|
|||||||
},
|
},
|
||||||
services: {
|
services: {
|
||||||
'Resolve hotkey by prefix': (context, event) => {
|
'Resolve hotkey by prefix': (context, event) => {
|
||||||
// First determine if we have a mouse or keyboard event
|
const resolvedInteraction = resolveInteractionEvent(event.data)
|
||||||
const action =
|
|
||||||
'key' in event.data
|
|
||||||
? mapKey(event.data.code)
|
|
||||||
: mouseButtonToName(event.data.button)
|
|
||||||
|
|
||||||
console.log('action', action)
|
|
||||||
|
|
||||||
// if the key is already a modifier key, skip everything else and reject
|
// 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
|
// We return an empty string so that we don't clear the currentSequence
|
||||||
return Promise.reject('')
|
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
|
// Find all the sequences that start with the current sequence
|
||||||
const searchString =
|
const searchString =
|
||||||
(context.currentSequence ? context.currentSequence + ' ' : '') + step
|
(context.currentSequence ? context.currentSequence + ' ' : '') +
|
||||||
|
resolvedInteraction.asString
|
||||||
|
|
||||||
const matches = context.interactionMap.filter((item) =>
|
const matches = context.interactionMap.filter((item) =>
|
||||||
item.sequence.startsWith(searchString)
|
item.sequence.startsWith(searchString)
|
||||||
@ -141,7 +132,7 @@ export function InteractionMapMachineProvider({
|
|||||||
// We have a prefix match.
|
// We have a prefix match.
|
||||||
// Reject the promise and return the step
|
// Reject the promise and return the step
|
||||||
// so we can add it to currentSequence
|
// so we can add it to currentSequence
|
||||||
return Promise.reject(step)
|
return Promise.reject(resolvedInteraction.asString)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve to just one exact match
|
// Resolve to just one exact match
|
||||||
@ -211,16 +202,3 @@ export function InteractionMapMachineProvider({
|
|||||||
</InteractionMapMachineContext.Provider>
|
</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 { 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() {
|
export function AllKeybindingsFields() {
|
||||||
const { state } = useInteractionMapContext()
|
const { state } = useInteractionMapContext()
|
||||||
@ -6,24 +16,81 @@ export function AllKeybindingsFields() {
|
|||||||
<div className="relative overflow-y-auto">
|
<div className="relative overflow-y-auto">
|
||||||
<div className="flex flex-col gap-4 px-2">
|
<div className="flex flex-col gap-4 px-2">
|
||||||
{state.context.interactionMap.map((item) => (
|
{state.context.interactionMap.map((item) => (
|
||||||
<div
|
<KeybindingField key={item.ownerId + '-' + item.name} item={item} />
|
||||||
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>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</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 = '+'
|
export const INTERACTION_MAP_SEPARATOR = '+'
|
||||||
/** The default KCL length expression */
|
/** The default KCL length expression */
|
||||||
export const KCL_DEFAULT_LENGTH = `5`
|
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
|
* 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
|
* 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) {
|
export function sortKeys(a: string, b: string) {
|
||||||
return isModifierKey(a) ? -1 : isModifierKey(b) ? 1 : a.localeCompare(b)
|
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