Basic editable (not persisted) keybindings

This commit is contained in:
Frank Noirot
2024-08-01 15:04:35 -04:00
parent b5f2e0ea3e
commit cac848ab22
9 changed files with 230 additions and 95 deletions

48
src-tauri/Cargo.lock generated
View File

@ -188,7 +188,7 @@ dependencies = [
"tauri-plugin-shell", "tauri-plugin-shell",
"tauri-plugin-updater", "tauri-plugin-updater",
"tokio", "tokio",
"toml 0.8.16", "toml 0.8.19",
"url", "url",
] ]
@ -727,7 +727,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a969e13a7589e9e3e4207e153bae624ade2b5622fb4684a4923b23ec3d57719" checksum = "8a969e13a7589e9e3e4207e153bae624ade2b5622fb4684a4923b23ec3d57719"
dependencies = [ dependencies = [
"serde", "serde",
"toml 0.8.16", "toml 0.8.19",
] ]
[[package]] [[package]]
@ -798,9 +798,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.11" version = "4.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3" checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -808,9 +808,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.11" version = "4.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa" checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -822,9 +822,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.11" version = "4.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e" checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@ -1389,7 +1389,7 @@ dependencies = [
"cc", "cc",
"memchr", "memchr",
"rustc_version", "rustc_version",
"toml 0.8.16", "toml 0.8.19",
"vswhom", "vswhom",
"winreg 0.52.0", "winreg 0.52.0",
] ]
@ -2622,7 +2622,7 @@ dependencies = [
"thiserror", "thiserror",
"tokio", "tokio",
"tokio-tungstenite", "tokio-tungstenite",
"toml 0.8.16", "toml 0.8.19",
"tower-lsp", "tower-lsp",
"ts-rs", "ts-rs",
"url", "url",
@ -5088,7 +5088,7 @@ dependencies = [
"cfg-expr", "cfg-expr",
"heck 0.5.0", "heck 0.5.0",
"pkg-config", "pkg-config",
"toml 0.8.16", "toml 0.8.19",
"version-compare", "version-compare",
] ]
@ -5241,7 +5241,7 @@ dependencies = [
"serde_json", "serde_json",
"tauri-utils", "tauri-utils",
"tauri-winres", "tauri-winres",
"toml 0.8.16", "toml 0.8.19",
"walkdir", "walkdir",
] ]
@ -5299,7 +5299,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"tauri-utils", "tauri-utils",
"toml 0.8.16", "toml 0.8.19",
"walkdir", "walkdir",
] ]
@ -5583,7 +5583,7 @@ dependencies = [
"serde_with", "serde_with",
"swift-rs", "swift-rs",
"thiserror", "thiserror",
"toml 0.8.16", "toml 0.8.19",
"url", "url",
"urlpattern", "urlpattern",
"walkdir", "walkdir",
@ -5830,21 +5830,21 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.8.16" version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81967dd0dd2c1ab0bc3468bd7caecc32b8a4aa47d0c8c695d8c2b2108168d62c" checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
dependencies = [ dependencies = [
"serde", "serde",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime",
"toml_edit 0.22.17", "toml_edit 0.22.20",
] ]
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.6.7" version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fb9f64314842840f1d940ac544da178732128f1c78c21772e876579e0da1db" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@ -5886,15 +5886,15 @@ dependencies = [
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.22.17" version = "0.22.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d9f8729f5aea9562aac1cc0441f5d6de3cff1ee0c5d67293eeca5eb36ee7c16" checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
dependencies = [ dependencies = [
"indexmap 2.2.6", "indexmap 2.2.6",
"serde", "serde",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime",
"winnow 0.6.6", "winnow 0.6.18",
] ]
[[package]] [[package]]
@ -6965,9 +6965,9 @@ dependencies = [
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.6.6" version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]

View File

@ -10,7 +10,6 @@ import { isSingleCursorInPipe } from 'lang/queryAst'
import { useShouldDisableModelingActions } from 'hooks/useShouldDisableModelingActions' import { useShouldDisableModelingActions } from 'hooks/useShouldDisableModelingActions'
import { useInteractionMap } from 'hooks/useInteractionMap' import { useInteractionMap } from 'hooks/useInteractionMap'
import { ActionButtonDropdown } from 'components/ActionButtonDropdown' import { ActionButtonDropdown } from 'components/ActionButtonDropdown'
import { useHotkeys } from 'react-hotkeys-hook'
import Tooltip from 'components/Tooltip' import Tooltip from 'components/Tooltip'
import { KEYBINDING_CATEGORIES } from 'lib/constants' import { KEYBINDING_CATEGORIES } from 'lib/constants'
import { useAppState } from 'AppState' import { useAppState } from 'AppState'
@ -30,7 +29,6 @@ export function Toolbar({
}: React.HTMLAttributes<HTMLElement>) { }: React.HTMLAttributes<HTMLElement>) {
const { state, send, context } = useModelingContext() const { state, send, context } = useModelingContext()
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const shouldDisableModelingActions = useShouldDisableModelingActions()
useInteractionMap( useInteractionMap(
[ [
{ {
@ -39,7 +37,7 @@ export function Toolbar({
sequence: 'shift+s', sequence: 'shift+s',
action: () => action: () =>
send({ type: 'Enter sketch', data: { forceNewSketch: true } }), send({ type: 'Enter sketch', data: { forceNewSketch: true } }),
guard: () => !shouldDisableModelingActions && state.matches('idle'), guard: () => state.can('Enter sketch'),
}, },
{ {
name: 'extrude', name: 'extrude',
@ -50,10 +48,10 @@ export function Toolbar({
type: 'Find and select command', type: 'Find and select command',
data: { name: 'Extrude', groupId: 'modeling' }, data: { name: 'Extrude', groupId: 'modeling' },
}), }),
guard: () => !shouldDisableModelingActions && state.matches('idle'), guard: () => state.can('Extrude'),
}, },
], ],
[shouldDisableModelingActions, commandBarSend, state], [commandBarSend, state],
KEYBINDING_CATEGORIES.MODELING KEYBINDING_CATEGORIES.MODELING
) )
const iconClassName = const iconClassName =
@ -286,19 +284,19 @@ const ToolbarItemContents = memo(function ToolbarItemContents({
itemConfig: ToolbarItemResolved itemConfig: ToolbarItemResolved
configCallbackProps: ToolbarItemCallbackProps configCallbackProps: ToolbarItemCallbackProps
}) { }) {
useHotkeys( // useHotkeys(
itemConfig.hotkey || '', // itemConfig.hotkey || '',
() => { // () => {
itemConfig.onClick(configCallbackProps) // itemConfig.onClick(configCallbackProps)
}, // },
{ // {
enabled: // enabled:
itemConfig.status === 'available' && // itemConfig.status === 'available' &&
!!itemConfig.hotkey && // !!itemConfig.hotkey &&
!itemConfig.disabled && // !itemConfig.disabled &&
!itemConfig.disableHotkey, // !itemConfig.disableHotkey,
} // }
) // )
return ( return (
<> <>

View File

@ -10,6 +10,7 @@ import {
import { import {
MouseButtonName, MouseButtonName,
interactionMapMachine, interactionMapMachine,
makeOverrideKey,
} from 'machines/interactionMapMachine' } from 'machines/interactionMapMachine'
import { createContext, useEffect } from 'react' import { createContext, useEffect } from 'react'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
@ -61,7 +62,9 @@ export function InteractionMapMachineProvider({
// normalize any interaction sequences to be sorted // normalize any interaction sequences to be sorted
const normalizedInteractions = event.data.map((item) => ({ const normalizedInteractions = event.data.map((item) => ({
...item, ...item,
sequence: item.sequence sequence: (
context.overrides[makeOverrideKey(item)] || item.sequence
)
.split(' ') .split(' ')
.map((step) => .map((step) =>
step step
@ -94,6 +97,17 @@ export function InteractionMapMachineProvider({
] ]
}, },
}), }),
'Merge into overrides': assign({
overrides: (context, event) => {
return {
...context.overrides,
...event.data,
}
},
}),
'Persist keybinding overrides': (context) => {
console.log('Persisting keybinding overrides', context.overrides)
},
}, },
services: { services: {
'Resolve hotkey by prefix': (context, event) => { 'Resolve hotkey by prefix': (context, event) => {
@ -111,13 +125,16 @@ export function InteractionMapMachineProvider({
resolvedInteraction.asString resolvedInteraction.asString
const matches = context.interactionMap.filter((item) => const matches = context.interactionMap.filter((item) =>
item.sequence.startsWith(searchString) (
context.overrides[makeOverrideKey(item)] || item.sequence
).startsWith(searchString)
) )
console.log('matches', { console.log('matches', {
matches, matches,
interactionMap: context.interactionMap, interactionMap: context.interactionMap,
searchString, searchString,
overrides: context.overrides,
}) })
// If we have no matches, reject the promise // If we have no matches, reject the promise
@ -126,8 +143,11 @@ export function InteractionMapMachineProvider({
} }
const exactMatches = matches.filter( const exactMatches = matches.filter(
(item) => item.sequence === searchString (item) =>
(context.overrides[makeOverrideKey(item)] || item.sequence) ===
searchString
) )
console.log('exactMatches', exactMatches)
if (!exactMatches.length) { if (!exactMatches.length) {
// We have a prefix match. // We have a prefix match.
// Reject the promise and return the step // Reject the promise and return the step
@ -136,9 +156,11 @@ export function InteractionMapMachineProvider({
} }
// Resolve to just one exact match // Resolve to just one exact match
const availableExactMatches = exactMatches.filter((item) => const availableExactMatches = exactMatches.filter(
item.guard(event.data) (item) => !item.guard || item.guard(event.data)
) )
console.log('availableExactMatches', availableExactMatches)
if (availableExactMatches.length === 0) { if (availableExactMatches.length === 0) {
return Promise.reject() return Promise.reject()
} else { } else {

View File

@ -26,7 +26,7 @@ export type SidebarType =
| 'lspMessages' | 'lspMessages'
| 'variables' | 'variables'
const PANE_KEYBINDING_PREFIX = 'alt+p ' as const const PANE_KEYBINDING_PREFIX = 'ctrl+shift+p ' as const
/** /**
* This interface can be extended as more context is needed for the panes * This interface can be extended as more context is needed for the panes

View File

@ -12,6 +12,10 @@ import { CustomIconName } from 'components/CustomIcon'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { IconDefinition } from '@fortawesome/free-solid-svg-icons' import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { KEYBINDING_CATEGORIES } from 'lib/constants'
import { useInteractionMap } from 'hooks/useInteractionMap'
import { useInteractionMapContext } from 'hooks/useInteractionMapContext'
import { InteractionSequence } from 'components/Settings/AllKeybindingsFields'
interface ModelingSidebarProps { interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40' paneOpacity: '' | 'opacity-20' | 'opacity-40'
@ -63,6 +67,18 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
[sidebarPanes, showDebugPanel.current] [sidebarPanes, showDebugPanel.current]
) )
useInteractionMap(
filteredPanes.map((pane) => ({
name: pane.id,
action: () => togglePane(pane.id),
keybinding: pane.keybinding,
title: `Toggle ${pane.title} pane`,
sequence: pane.keybinding,
})),
[filteredPanes, context.store?.openPanes],
KEYBINDING_CATEGORIES.USER_INTERFACE
)
const paneBadgeMap: Record<SidebarType, number | boolean> = useMemo(() => { const paneBadgeMap: Record<SidebarType, number | boolean> = useMemo(() => {
return filteredPanes.reduce((acc, pane) => { return filteredPanes.reduce((acc, pane) => {
if (pane.showBadge) { if (pane.showBadge) {
@ -207,6 +223,16 @@ function ModelingPaneButton({
showBadge, showBadge,
...props ...props
}: ModelingPaneButtonProps) { }: ModelingPaneButtonProps) {
const { state: interactionMapState } = useInteractionMapContext()
const resolvedKeybinding = useMemo(
() =>
interactionMapState.context.overrides[
`${KEYBINDING_CATEGORIES.USER_INTERFACE}.${paneConfig.id}`
] || paneConfig.keybinding,
[interactionMapState.context.overrides]
)
return ( return (
<button <button
className="pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent p-0 m-0 rounded-sm !outline-0 focus-visible:border-primary" className="pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent p-0 m-0 rounded-sm !outline-0 focus-visible:border-primary"
@ -258,7 +284,10 @@ function ModelingPaneButton({
{paneConfig.title} {paneConfig.title}
{paneIsOpen !== undefined ? ` pane` : ''} {paneIsOpen !== undefined ? ` pane` : ''}
</span> </span>
<kbd className="hotkey text-xs capitalize">{paneConfig.keybinding}</kbd> <InteractionSequence
sequence={resolvedKeybinding}
className="flex-nowrap !gap-1"
/>
</Tooltip> </Tooltip>
</button> </button>
) )

View File

@ -1,13 +1,11 @@
import { ActionIcon } from 'components/ActionIcon' import { ActionIcon } from 'components/ActionIcon'
import { useInteractionMapContext } from 'hooks/useInteractionMapContext' import { useInteractionMapContext } from 'hooks/useInteractionMapContext'
import { resolveInteractionEvent } from 'lib/keyboard'
import { import {
isModifierKey, InteractionMapItem,
mapKey, makeOverrideKey,
mouseButtonToName, } from 'machines/interactionMapMachine'
resolveInteractionEvent, import { FormEvent, HTMLProps, useEffect, useRef, useState } from 'react'
} from 'lib/keyboard'
import { InteractionMapItem } from 'machines/interactionMapMachine'
import { useEffect, useState } from 'react'
export function AllKeybindingsFields() { export function AllKeybindingsFields() {
const { state } = useInteractionMapContext() const { state } = useInteractionMapContext()
@ -23,8 +21,23 @@ export function AllKeybindingsFields() {
} }
function KeybindingField({ item }: { item: InteractionMapItem }) { function KeybindingField({ item }: { item: InteractionMapItem }) {
const { send, state } = useInteractionMapContext()
const [isEditing, setIsEditing] = useState(false) const [isEditing, setIsEditing] = useState(false)
const [newSequence, setNewSequence] = useState('') const [newSequence, setNewSequence] = useState('')
const submitRef = useRef<HTMLButtonElement>(null)
function handleSubmit(e: FormEvent) {
e.preventDefault()
if (newSequence !== item.sequence) {
send({
type: 'Update overrides',
data: {
[makeOverrideKey(item)]: newSequence,
},
})
}
setIsEditing(false)
}
useEffect(() => { useEffect(() => {
const blockOtherEvents = (e: KeyboardEvent | MouseEvent) => { const blockOtherEvents = (e: KeyboardEvent | MouseEvent) => {
@ -34,13 +47,25 @@ function KeybindingField({ item }: { item: InteractionMapItem }) {
} }
const handleInteraction = (e: KeyboardEvent | MouseEvent) => { const handleInteraction = (e: KeyboardEvent | MouseEvent) => {
if (e instanceof KeyboardEvent && e.key === 'Escape') {
blockOtherEvents(e)
setIsEditing(false)
return
} else if (e instanceof KeyboardEvent && e.key === 'Enter') {
return
} else if (e instanceof MouseEvent && e.target === submitRef.current) {
return
}
blockOtherEvents(e) blockOtherEvents(e)
const resolvedInteraction = resolveInteractionEvent(e) const resolvedInteraction = resolveInteractionEvent(e)
if (resolvedInteraction.isModifier) return if (resolvedInteraction.isModifier) return
setNewSequence( setNewSequence((prev) => {
(prev) => prev + (prev.length ? ' ' : '') + resolvedInteraction.asString const newSequence =
) prev + (prev.length ? ' ' : '') + resolvedInteraction.asString
console.log('newSequence', newSequence)
return newSequence
})
} }
const handleContextMenu = (e: MouseEvent) => { const handleContextMenu = (e: MouseEvent) => {
@ -49,47 +74,100 @@ function KeybindingField({ item }: { item: InteractionMapItem }) {
if (!isEditing) { if (!isEditing) {
setNewSequence('') setNewSequence('')
globalThis?.window?.removeEventListener('keydown', handleInteraction) globalThis?.window?.removeEventListener('keydown', handleInteraction, {
globalThis?.window?.removeEventListener('mousedown', handleInteraction) capture: true,
globalThis?.window?.removeEventListener('contextmenu', handleContextMenu) })
globalThis?.window?.removeEventListener('mousedown', handleInteraction, {
capture: true,
})
globalThis?.window?.removeEventListener(
'contextmenu',
handleContextMenu,
{ capture: true }
)
} else { } else {
globalThis?.window?.addEventListener('keydown', handleInteraction) globalThis?.window?.addEventListener('keydown', handleInteraction, {
globalThis?.window?.addEventListener('mousedown', handleInteraction) capture: true,
globalThis?.window?.addEventListener('contextmenu', handleContextMenu) })
globalThis?.window?.addEventListener('mousedown', handleInteraction, {
capture: true,
})
globalThis?.window?.addEventListener('contextmenu', handleContextMenu, {
capture: true,
})
} }
return () => { return () => {
globalThis?.window?.removeEventListener('keydown', handleInteraction) globalThis?.window?.removeEventListener('keydown', handleInteraction, {
globalThis?.window?.removeEventListener('mousedown', handleInteraction) capture: true,
globalThis?.window?.removeEventListener('contextmenu', handleContextMenu) })
globalThis?.window?.removeEventListener('mousedown', handleInteraction, {
capture: true,
})
globalThis?.window?.removeEventListener(
'contextmenu',
handleContextMenu,
{ capture: true }
)
} }
}, [isEditing]) }, [isEditing, setNewSequence])
return ( return isEditing ? (
<form
key={item.ownerId + '-' + item.name}
className="flex gap-2 justify-between items-start"
onSubmit={handleSubmit}
>
<h3>{item.title}</h3>
<InteractionSequence sequence={newSequence} showNoSequence />
<input type="hidden" value={item.sequence} name="sequence" />
<button ref={submitRef} className="p-0 m-0" type="submit">
<ActionIcon icon="checkmark" />
</button>
</form>
) : (
<div <div
key={item.ownerId + '-' + item.name} key={item.ownerId + '-' + item.name}
className="flex gap-2 justify-between items-start" className="flex gap-2 justify-between items-start"
> >
<h3>{item.title}</h3> <h3>{item.title}</h3>
<div className="flex-1 flex flex-wrap justify-end gap-3"> <InteractionSequence
{(isEditing ? newSequence : item.sequence) sequence={
.split(' ') state.context.overrides[makeOverrideKey(item)] || item.sequence
.map((chord, i) => ( }
<kbd showNoSequence
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 <button
onClick={() => setIsEditing((prev) => !prev)} ref={submitRef}
className="p-0 m-0" className="p-0 m-0"
type={isEditing ? 'submit' : 'button'} onClick={() => setIsEditing(true)}
> >
<ActionIcon icon={isEditing ? 'checkmark' : 'sketch'} /> <ActionIcon icon="sketch" />
</button> </button>
</div> </div>
) )
} }
export function InteractionSequence({
sequence,
className = '',
showNoSequence = false,
...props
}: HTMLProps<HTMLDivElement> & { sequence: string; showNoSequence?: boolean }) {
return sequence.length ? (
<div
className={'flex-1 flex flex-wrap justify-end gap-3 ' + className}
{...props}
>
{sequence.split(' ').map((chord, i) => (
<kbd key={`sequence-${sequence}-${chord}-${i}`} className="hotkey">
{chord}
</kbd>
))}
</div>
) : (
showNoSequence && (
<div className="flex-1 flex justify-end text-xs">No sequence set</div>
)
)
}

View File

@ -1,9 +1,6 @@
import { import { useAppState } from 'AppState'
NetworkHealthState, import { NetworkHealthState, useNetworkStatus } from 'hooks/useNetworkStatus'
useNetworkStatus,
} from 'components/NetworkHealthIndicator'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { useStore } from 'useStore'
/** /**
* Custom hook to determine if modeling actions should be disabled * Custom hook to determine if modeling actions should be disabled
@ -13,9 +10,7 @@ import { useStore } from 'useStore'
export function useShouldDisableModelingActions() { export function useShouldDisableModelingActions() {
const { overallState } = useNetworkStatus() const { overallState } = useNetworkStatus()
const { isExecuting } = useKclContext() const { isExecuting } = useKclContext()
const { isStreamReady } = useStore((s) => ({ const { isStreamReady } = useAppState()
isStreamReady: s.isStreamReady,
}))
return overallState !== NetworkHealthState.Ok || isExecuting || !isStreamReady return overallState !== NetworkHealthState.Ok || isExecuting || !isStreamReady
} }

View File

@ -61,4 +61,5 @@ export const KCL_DEFAULT_LENGTH = `5`
export const KEYBINDING_CATEGORIES = { export const KEYBINDING_CATEGORIES = {
MODELING: 'modeling', MODELING: 'modeling',
COMMAND_BAR: 'command-bar', COMMAND_BAR: 'command-bar',
USER_INTERFACE: 'user-interface',
} }

View File

@ -6,15 +6,20 @@ export type InteractionMapItem = {
name: string name: string
title: string title: string
sequence: string sequence: string
guard: (e: MouseEvent | KeyboardEvent) => boolean guard?: (e: MouseEvent | KeyboardEvent) => boolean
action: () => void action: () => void
ownerId: string ownerId: string
} }
export function makeOverrideKey(interactionMapItem: InteractionMapItem) {
return `${interactionMapItem.ownerId}.${interactionMapItem.name}`
}
export const interactionMapMachine = createMachine({ export const interactionMapMachine = createMachine({
/** @xstate-layout N4IgpgJg5mDOIC5QEkB2AXMAnAhgY3QEsB7VAAgFkcAHMgQQOKwGI6IIz1izCNt8ipMgFsaAbQAMAXUShqxWIUGpZIAB6IAzAE4AbADoATBIAcAdk0AWCQFYJmiRMMAaEAE9EAWgCMN7-t0JbRNdS0tDM29dbQBfGNc0TFwCEnIqWgYuFgAlMGFiADcwMgAzLGJhHj5k5RFxaVV5RWVVDQRow30TU29rbrsrb1cPBB8-AKDtXytg7R0TOITqgVTKGnpGLH0AGUJYTFReKFKmKqSV0mYAMUIsYrAijEkZJBAmpVTWxDmbfTMI7wmEyWYGGbThYZaUJGKw2SxwmwWSJmRYgRL8FJCdIbLL6XKwYgAGyKZAAFsR0ABrMBuZgQUhgfS8ArEan6O4E4lgAASFOpbgAQm4AAp3EqENTPRoKD6kL4IbzeTSaLreMyWXS6RWaXRmOaQhB2Aw2KyGawOEyInWo9E1VbYzJMPFwIkk8lUmnMbDlLbUQk4dAlJjCdkurm8j2CkViiVS17vFqvNreCSArq6Yw6iLaRyGGwGqK-bRzUKGbyGM0mYuGG3LTFpdaOrb413Fd38r1YH36P0BoNYEMc1sR-lC0VgcWS7wvOQyxOgZNOfxWeHRDPzMsGgFdPSObrKsxwywo+Jouu1B2bfQAUTUYDwAFdMGR+aJaA8wBg6QymagWWywDvR9MAAaRpN9MlSONZ2aT4k0QMIzH0YJvGCGYbGCVNdANTQ4X0DVUzsCswW6WJT1tC4GwyK9b3vJ9ilfdYPy-b0nV7QNg30QC6NA8CaEg0hoLeOc4IXBCQQCGxjDVawKz1eEt0tdMHDMboU20M0zBPU9UGICA4FUCj6zWaismlWC5Xg0YhncRBfH0csdRTWwTEMaIbF0TRa3OYzL1xXZ9k-I4TiwM4MXnYSLJUKz-iQwwTFwzQEvhM1bC3DTVSBTzHBNdzvPC+1GyvFsuTJPkaXM2VorEhVj1+CRdBMctLHUk03JwzR-HLHMLECDyEu8fK7SxIrcVo4CGL499HnQSqIraY9FJVUJgQ1cJUP+CRLDiOIgA */ /** @xstate-layout N4IgpgJg5mDOIC5QEkB2AXMAnAhgY3QEsB7VAAgFkcAHMgQQOKwGI6IIz1izCNt8ipMgFsaAbQAMAXUShqxWIUGpZIAB6IAzAE4AbADoATBIAcAdk0AWCQFYJmiRMMAaEAE9EAWgCMN7-t0JbRNdS0tDM29dbQBfGNc0TFwCEnIqWgYuFgAlMGFiADcwMgAzLGJhHj5k5RFxaVV5RWVVDQRow30TU29rbrsrb1cPBB8-AKDtXytg7R0TOITqgVTKGnpGLH0AGUJYTFReKFKmKqSV0mYAMUIsYrAijEkZJBAmpVTWxDmbfTMI7wmEyWYGGbThYZaUJGKw2SxwmwWSJmRYgRL8FJCdIbLL6XKwYgAGyKZAAFsR0ABrMBuZgQUhgfS8ArEan6O4E4lgAASFOpbgAQm4AAp3EqENTPRoKD6kL4IbzeTSaLreMyWXS6RWaXRmOaQhB2Aw2KyGawOEyInWo9E1VbYzJMPFwIkk8lUmnMbDlLbUQk4dAlJjCdkurm8j2CkViiVS17vFqvNreCSArq6Yw6iLaRyGGwGqK-bRzUKGbyGM0mYuGG3LTFpdaOrb413Fd38r1YH36P0BoNYEMc1sR-lC0VgcWS7wvOQyxOgZNOfxWeHRDPzMsGgFdPSObrKsxwywo+Jouu1B2bfQAUTUYDwAFdMGR+aJaA8wBg6QymagWWywDvR9MAAaRpN9MlSONZ2aT4k0QMIzH0YJvGCGYbGCVNdANTQ4X0DVUzsCswW6WJT1tC4GwyK9b3vJ9ilfdYPy-b0nV7QNg30QC6NA8CaEg0hoLeOc4IXBCQQCGxjDVawKz1eEt0tdMHDMboU20M0zBPJZznrNZqKyZgAFVqAgANikKb1CAgOAhITUT1EQbUVRBA9gRsEw1SGdwvHLExkLLCR1yCzVyxPU9UGIGz4FeCi9MvLJpVguV4NGbyRl8fRyx1XCTDBQxNH+MJa10i9GyvXZ9k-I4TiwM4MXnYTkpUVL-iQwwTFwzROvhM1bC3DTVSBXQHFsHVtBsEqGvtcrcRbLkyT5GkktlFqxIVY9fiCzyzXUk1DGwnyEGVfxyxzCxAhsXROu8Ka7SxWanVo4CGL499HnQFbGraY9FJVUJgQ1cJUP+CRLDiOIgA */
context: { context: {
interactionMap: [] as InteractionMapItem[], interactionMap: [] as InteractionMapItem[],
overrides: {} as { [key: string]: string },
currentSequence: '' as string, currentSequence: '' as string,
}, },
predictableActionArguments: true, predictableActionArguments: true,
@ -35,6 +40,7 @@ export const interactionMapMachine = createMachine({
| { type: 'Update prefix matrix' } | { type: 'Update prefix matrix' }
| { type: 'Add last interaction to sequence' } | { type: 'Add last interaction to sequence' }
| { type: 'Clear sequence' } | { type: 'Clear sequence' }
| { type: 'Update overrides'; data: { [key: string]: string } }
| { type: 'Resolve hotkey by prefix'; data: MouseEvent | KeyboardEvent } | { type: 'Resolve hotkey by prefix'; data: MouseEvent | KeyboardEvent }
| { type: 'done.invoke.resolveHotkeyByPrefix'; data: InteractionMapItem } | { type: 'done.invoke.resolveHotkeyByPrefix'; data: InteractionMapItem }
| { | {
@ -115,5 +121,11 @@ export const interactionMapMachine = createMachine({
}, },
], ],
}, },
'Update overrides': {
target: '#Interaction Map Actor',
internal: true,
actions: ['Merge into overrides', 'Persist keybinding overrides'],
},
}, },
}) })