Basic editable (not persisted) keybindings
This commit is contained in:
		
							
								
								
									
										48
									
								
								src-tauri/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										48
									
								
								src-tauri/Cargo.lock
									
									
									
										generated
									
									
									
								
							@ -188,7 +188,7 @@ dependencies = [
 | 
			
		||||
 "tauri-plugin-shell",
 | 
			
		||||
 "tauri-plugin-updater",
 | 
			
		||||
 "tokio",
 | 
			
		||||
 "toml 0.8.16",
 | 
			
		||||
 "toml 0.8.19",
 | 
			
		||||
 "url",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
@ -727,7 +727,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "8a969e13a7589e9e3e4207e153bae624ade2b5622fb4684a4923b23ec3d57719"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "serde",
 | 
			
		||||
 "toml 0.8.16",
 | 
			
		||||
 "toml 0.8.19",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
@ -798,9 +798,9 @@ dependencies = [
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "clap"
 | 
			
		||||
version = "4.5.11"
 | 
			
		||||
version = "4.5.13"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3"
 | 
			
		||||
checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "clap_builder",
 | 
			
		||||
 "clap_derive",
 | 
			
		||||
@ -808,9 +808,9 @@ dependencies = [
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "clap_builder"
 | 
			
		||||
version = "4.5.11"
 | 
			
		||||
version = "4.5.13"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa"
 | 
			
		||||
checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "anstream",
 | 
			
		||||
 "anstyle",
 | 
			
		||||
@ -822,9 +822,9 @@ dependencies = [
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "clap_derive"
 | 
			
		||||
version = "4.5.11"
 | 
			
		||||
version = "4.5.13"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e"
 | 
			
		||||
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "heck 0.5.0",
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
@ -1389,7 +1389,7 @@ dependencies = [
 | 
			
		||||
 "cc",
 | 
			
		||||
 "memchr",
 | 
			
		||||
 "rustc_version",
 | 
			
		||||
 "toml 0.8.16",
 | 
			
		||||
 "toml 0.8.19",
 | 
			
		||||
 "vswhom",
 | 
			
		||||
 "winreg 0.52.0",
 | 
			
		||||
]
 | 
			
		||||
@ -2622,7 +2622,7 @@ dependencies = [
 | 
			
		||||
 "thiserror",
 | 
			
		||||
 "tokio",
 | 
			
		||||
 "tokio-tungstenite",
 | 
			
		||||
 "toml 0.8.16",
 | 
			
		||||
 "toml 0.8.19",
 | 
			
		||||
 "tower-lsp",
 | 
			
		||||
 "ts-rs",
 | 
			
		||||
 "url",
 | 
			
		||||
@ -5088,7 +5088,7 @@ dependencies = [
 | 
			
		||||
 "cfg-expr",
 | 
			
		||||
 "heck 0.5.0",
 | 
			
		||||
 "pkg-config",
 | 
			
		||||
 "toml 0.8.16",
 | 
			
		||||
 "toml 0.8.19",
 | 
			
		||||
 "version-compare",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
@ -5241,7 +5241,7 @@ dependencies = [
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "tauri-utils",
 | 
			
		||||
 "tauri-winres",
 | 
			
		||||
 "toml 0.8.16",
 | 
			
		||||
 "toml 0.8.19",
 | 
			
		||||
 "walkdir",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
@ -5299,7 +5299,7 @@ dependencies = [
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "tauri-utils",
 | 
			
		||||
 "toml 0.8.16",
 | 
			
		||||
 "toml 0.8.19",
 | 
			
		||||
 "walkdir",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
@ -5583,7 +5583,7 @@ dependencies = [
 | 
			
		||||
 "serde_with",
 | 
			
		||||
 "swift-rs",
 | 
			
		||||
 "thiserror",
 | 
			
		||||
 "toml 0.8.16",
 | 
			
		||||
 "toml 0.8.19",
 | 
			
		||||
 "url",
 | 
			
		||||
 "urlpattern",
 | 
			
		||||
 "walkdir",
 | 
			
		||||
@ -5830,21 +5830,21 @@ dependencies = [
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "toml"
 | 
			
		||||
version = "0.8.16"
 | 
			
		||||
version = "0.8.19"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "81967dd0dd2c1ab0bc3468bd7caecc32b8a4aa47d0c8c695d8c2b2108168d62c"
 | 
			
		||||
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_spanned",
 | 
			
		||||
 "toml_datetime",
 | 
			
		||||
 "toml_edit 0.22.17",
 | 
			
		||||
 "toml_edit 0.22.20",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "toml_datetime"
 | 
			
		||||
version = "0.6.7"
 | 
			
		||||
version = "0.6.8"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "f8fb9f64314842840f1d940ac544da178732128f1c78c21772e876579e0da1db"
 | 
			
		||||
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "serde",
 | 
			
		||||
]
 | 
			
		||||
@ -5886,15 +5886,15 @@ dependencies = [
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "toml_edit"
 | 
			
		||||
version = "0.22.17"
 | 
			
		||||
version = "0.22.20"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "8d9f8729f5aea9562aac1cc0441f5d6de3cff1ee0c5d67293eeca5eb36ee7c16"
 | 
			
		||||
checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "indexmap 2.2.6",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_spanned",
 | 
			
		||||
 "toml_datetime",
 | 
			
		||||
 "winnow 0.6.6",
 | 
			
		||||
 "winnow 0.6.18",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
@ -6965,9 +6965,9 @@ dependencies = [
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "winnow"
 | 
			
		||||
version = "0.6.6"
 | 
			
		||||
version = "0.6.18"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352"
 | 
			
		||||
checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "memchr",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,6 @@ import { isSingleCursorInPipe } from 'lang/queryAst'
 | 
			
		||||
import { useShouldDisableModelingActions } from 'hooks/useShouldDisableModelingActions'
 | 
			
		||||
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'
 | 
			
		||||
import { useAppState } from 'AppState'
 | 
			
		||||
@ -30,7 +29,6 @@ export function Toolbar({
 | 
			
		||||
}: React.HTMLAttributes<HTMLElement>) {
 | 
			
		||||
  const { state, send, context } = useModelingContext()
 | 
			
		||||
  const { commandBarSend } = useCommandsContext()
 | 
			
		||||
  const shouldDisableModelingActions = useShouldDisableModelingActions()
 | 
			
		||||
  useInteractionMap(
 | 
			
		||||
    [
 | 
			
		||||
      {
 | 
			
		||||
@ -39,7 +37,7 @@ export function Toolbar({
 | 
			
		||||
        sequence: 'shift+s',
 | 
			
		||||
        action: () =>
 | 
			
		||||
          send({ type: 'Enter sketch', data: { forceNewSketch: true } }),
 | 
			
		||||
        guard: () => !shouldDisableModelingActions && state.matches('idle'),
 | 
			
		||||
        guard: () => state.can('Enter sketch'),
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'extrude',
 | 
			
		||||
@ -50,10 +48,10 @@ export function Toolbar({
 | 
			
		||||
            type: 'Find and select command',
 | 
			
		||||
            data: { name: 'Extrude', groupId: 'modeling' },
 | 
			
		||||
          }),
 | 
			
		||||
        guard: () => !shouldDisableModelingActions && state.matches('idle'),
 | 
			
		||||
        guard: () => state.can('Extrude'),
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
    [shouldDisableModelingActions, commandBarSend, state],
 | 
			
		||||
    [commandBarSend, state],
 | 
			
		||||
    KEYBINDING_CATEGORIES.MODELING
 | 
			
		||||
  )
 | 
			
		||||
  const iconClassName =
 | 
			
		||||
@ -286,19 +284,19 @@ const ToolbarItemContents = memo(function ToolbarItemContents({
 | 
			
		||||
  itemConfig: ToolbarItemResolved
 | 
			
		||||
  configCallbackProps: ToolbarItemCallbackProps
 | 
			
		||||
}) {
 | 
			
		||||
  useHotkeys(
 | 
			
		||||
    itemConfig.hotkey || '',
 | 
			
		||||
    () => {
 | 
			
		||||
      itemConfig.onClick(configCallbackProps)
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      enabled:
 | 
			
		||||
        itemConfig.status === 'available' &&
 | 
			
		||||
        !!itemConfig.hotkey &&
 | 
			
		||||
        !itemConfig.disabled &&
 | 
			
		||||
        !itemConfig.disableHotkey,
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
  // useHotkeys(
 | 
			
		||||
  //   itemConfig.hotkey || '',
 | 
			
		||||
  //   () => {
 | 
			
		||||
  //     itemConfig.onClick(configCallbackProps)
 | 
			
		||||
  //   },
 | 
			
		||||
  //   {
 | 
			
		||||
  //     enabled:
 | 
			
		||||
  //       itemConfig.status === 'available' &&
 | 
			
		||||
  //       !!itemConfig.hotkey &&
 | 
			
		||||
  //       !itemConfig.disabled &&
 | 
			
		||||
  //       !itemConfig.disableHotkey,
 | 
			
		||||
  //   }
 | 
			
		||||
  // )
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,7 @@ import {
 | 
			
		||||
import {
 | 
			
		||||
  MouseButtonName,
 | 
			
		||||
  interactionMapMachine,
 | 
			
		||||
  makeOverrideKey,
 | 
			
		||||
} from 'machines/interactionMapMachine'
 | 
			
		||||
import { createContext, useEffect } from 'react'
 | 
			
		||||
import toast from 'react-hot-toast'
 | 
			
		||||
@ -61,7 +62,9 @@ export function InteractionMapMachineProvider({
 | 
			
		||||
          // normalize any interaction sequences to be sorted
 | 
			
		||||
          const normalizedInteractions = event.data.map((item) => ({
 | 
			
		||||
            ...item,
 | 
			
		||||
            sequence: item.sequence
 | 
			
		||||
            sequence: (
 | 
			
		||||
              context.overrides[makeOverrideKey(item)] || item.sequence
 | 
			
		||||
            )
 | 
			
		||||
              .split(' ')
 | 
			
		||||
              .map((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: {
 | 
			
		||||
      'Resolve hotkey by prefix': (context, event) => {
 | 
			
		||||
@ -111,13 +125,16 @@ export function InteractionMapMachineProvider({
 | 
			
		||||
          resolvedInteraction.asString
 | 
			
		||||
 | 
			
		||||
        const matches = context.interactionMap.filter((item) =>
 | 
			
		||||
          item.sequence.startsWith(searchString)
 | 
			
		||||
          (
 | 
			
		||||
            context.overrides[makeOverrideKey(item)] || item.sequence
 | 
			
		||||
          ).startsWith(searchString)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        console.log('matches', {
 | 
			
		||||
          matches,
 | 
			
		||||
          interactionMap: context.interactionMap,
 | 
			
		||||
          searchString,
 | 
			
		||||
          overrides: context.overrides,
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        // If we have no matches, reject the promise
 | 
			
		||||
@ -126,8 +143,11 @@ export function InteractionMapMachineProvider({
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const exactMatches = matches.filter(
 | 
			
		||||
          (item) => item.sequence === searchString
 | 
			
		||||
          (item) =>
 | 
			
		||||
            (context.overrides[makeOverrideKey(item)] || item.sequence) ===
 | 
			
		||||
            searchString
 | 
			
		||||
        )
 | 
			
		||||
        console.log('exactMatches', exactMatches)
 | 
			
		||||
        if (!exactMatches.length) {
 | 
			
		||||
          // We have a prefix match.
 | 
			
		||||
          // Reject the promise and return the step
 | 
			
		||||
@ -136,9 +156,11 @@ export function InteractionMapMachineProvider({
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Resolve to just one exact match
 | 
			
		||||
        const availableExactMatches = exactMatches.filter((item) =>
 | 
			
		||||
          item.guard(event.data)
 | 
			
		||||
        const availableExactMatches = exactMatches.filter(
 | 
			
		||||
          (item) => !item.guard || item.guard(event.data)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        console.log('availableExactMatches', availableExactMatches)
 | 
			
		||||
        if (availableExactMatches.length === 0) {
 | 
			
		||||
          return Promise.reject()
 | 
			
		||||
        } else {
 | 
			
		||||
 | 
			
		||||
@ -26,7 +26,7 @@ export type SidebarType =
 | 
			
		||||
  | 'lspMessages'
 | 
			
		||||
  | '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
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,10 @@ import { CustomIconName } from 'components/CustomIcon'
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
 | 
			
		||||
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 {
 | 
			
		||||
  paneOpacity: '' | 'opacity-20' | 'opacity-40'
 | 
			
		||||
@ -63,6 +67,18 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
 | 
			
		||||
    [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(() => {
 | 
			
		||||
    return filteredPanes.reduce((acc, pane) => {
 | 
			
		||||
      if (pane.showBadge) {
 | 
			
		||||
@ -207,6 +223,16 @@ function ModelingPaneButton({
 | 
			
		||||
  showBadge,
 | 
			
		||||
  ...props
 | 
			
		||||
}: ModelingPaneButtonProps) {
 | 
			
		||||
  const { state: interactionMapState } = useInteractionMapContext()
 | 
			
		||||
 | 
			
		||||
  const resolvedKeybinding = useMemo(
 | 
			
		||||
    () =>
 | 
			
		||||
      interactionMapState.context.overrides[
 | 
			
		||||
        `${KEYBINDING_CATEGORIES.USER_INTERFACE}.${paneConfig.id}`
 | 
			
		||||
      ] || paneConfig.keybinding,
 | 
			
		||||
    [interactionMapState.context.overrides]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <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"
 | 
			
		||||
@ -258,7 +284,10 @@ function ModelingPaneButton({
 | 
			
		||||
          {paneConfig.title}
 | 
			
		||||
          {paneIsOpen !== undefined ? ` pane` : ''}
 | 
			
		||||
        </span>
 | 
			
		||||
        <kbd className="hotkey text-xs capitalize">{paneConfig.keybinding}</kbd>
 | 
			
		||||
        <InteractionSequence
 | 
			
		||||
          sequence={resolvedKeybinding}
 | 
			
		||||
          className="flex-nowrap !gap-1"
 | 
			
		||||
        />
 | 
			
		||||
      </Tooltip>
 | 
			
		||||
    </button>
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,11 @@
 | 
			
		||||
import { ActionIcon } from 'components/ActionIcon'
 | 
			
		||||
import { useInteractionMapContext } from 'hooks/useInteractionMapContext'
 | 
			
		||||
import { resolveInteractionEvent } from 'lib/keyboard'
 | 
			
		||||
import {
 | 
			
		||||
  isModifierKey,
 | 
			
		||||
  mapKey,
 | 
			
		||||
  mouseButtonToName,
 | 
			
		||||
  resolveInteractionEvent,
 | 
			
		||||
} from 'lib/keyboard'
 | 
			
		||||
import { InteractionMapItem } from 'machines/interactionMapMachine'
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
  InteractionMapItem,
 | 
			
		||||
  makeOverrideKey,
 | 
			
		||||
} from 'machines/interactionMapMachine'
 | 
			
		||||
import { FormEvent, HTMLProps, useEffect, useRef, useState } from 'react'
 | 
			
		||||
 | 
			
		||||
export function AllKeybindingsFields() {
 | 
			
		||||
  const { state } = useInteractionMapContext()
 | 
			
		||||
@ -23,8 +21,23 @@ export function AllKeybindingsFields() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function KeybindingField({ item }: { item: InteractionMapItem }) {
 | 
			
		||||
  const { send, state } = useInteractionMapContext()
 | 
			
		||||
  const [isEditing, setIsEditing] = useState(false)
 | 
			
		||||
  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(() => {
 | 
			
		||||
    const blockOtherEvents = (e: KeyboardEvent | MouseEvent) => {
 | 
			
		||||
@ -34,13 +47,25 @@ function KeybindingField({ item }: { item: InteractionMapItem }) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
 | 
			
		||||
      const resolvedInteraction = resolveInteractionEvent(e)
 | 
			
		||||
      if (resolvedInteraction.isModifier) return
 | 
			
		||||
      setNewSequence(
 | 
			
		||||
        (prev) => prev + (prev.length ? ' ' : '') + resolvedInteraction.asString
 | 
			
		||||
      )
 | 
			
		||||
      setNewSequence((prev) => {
 | 
			
		||||
        const newSequence =
 | 
			
		||||
          prev + (prev.length ? ' ' : '') + resolvedInteraction.asString
 | 
			
		||||
        console.log('newSequence', newSequence)
 | 
			
		||||
        return newSequence
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const handleContextMenu = (e: MouseEvent) => {
 | 
			
		||||
@ -49,47 +74,100 @@ function KeybindingField({ item }: { item: InteractionMapItem }) {
 | 
			
		||||
 | 
			
		||||
    if (!isEditing) {
 | 
			
		||||
      setNewSequence('')
 | 
			
		||||
      globalThis?.window?.removeEventListener('keydown', handleInteraction)
 | 
			
		||||
      globalThis?.window?.removeEventListener('mousedown', handleInteraction)
 | 
			
		||||
      globalThis?.window?.removeEventListener('contextmenu', handleContextMenu)
 | 
			
		||||
      globalThis?.window?.removeEventListener('keydown', handleInteraction, {
 | 
			
		||||
        capture: true,
 | 
			
		||||
      })
 | 
			
		||||
      globalThis?.window?.removeEventListener('mousedown', handleInteraction, {
 | 
			
		||||
        capture: true,
 | 
			
		||||
      })
 | 
			
		||||
      globalThis?.window?.removeEventListener(
 | 
			
		||||
        'contextmenu',
 | 
			
		||||
        handleContextMenu,
 | 
			
		||||
        { capture: true }
 | 
			
		||||
      )
 | 
			
		||||
    } else {
 | 
			
		||||
      globalThis?.window?.addEventListener('keydown', handleInteraction)
 | 
			
		||||
      globalThis?.window?.addEventListener('mousedown', handleInteraction)
 | 
			
		||||
      globalThis?.window?.addEventListener('contextmenu', handleContextMenu)
 | 
			
		||||
      globalThis?.window?.addEventListener('keydown', handleInteraction, {
 | 
			
		||||
        capture: true,
 | 
			
		||||
      })
 | 
			
		||||
      globalThis?.window?.addEventListener('mousedown', handleInteraction, {
 | 
			
		||||
        capture: true,
 | 
			
		||||
      })
 | 
			
		||||
      globalThis?.window?.addEventListener('contextmenu', handleContextMenu, {
 | 
			
		||||
        capture: true,
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      globalThis?.window?.removeEventListener('keydown', handleInteraction)
 | 
			
		||||
      globalThis?.window?.removeEventListener('mousedown', handleInteraction)
 | 
			
		||||
      globalThis?.window?.removeEventListener('contextmenu', handleContextMenu)
 | 
			
		||||
      globalThis?.window?.removeEventListener('keydown', handleInteraction, {
 | 
			
		||||
        capture: true,
 | 
			
		||||
      })
 | 
			
		||||
      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
 | 
			
		||||
      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>
 | 
			
		||||
      <InteractionSequence
 | 
			
		||||
        sequence={
 | 
			
		||||
          state.context.overrides[makeOverrideKey(item)] || item.sequence
 | 
			
		||||
        }
 | 
			
		||||
        showNoSequence
 | 
			
		||||
      />
 | 
			
		||||
      <button
 | 
			
		||||
        onClick={() => setIsEditing((prev) => !prev)}
 | 
			
		||||
        ref={submitRef}
 | 
			
		||||
        className="p-0 m-0"
 | 
			
		||||
        type={isEditing ? 'submit' : 'button'}
 | 
			
		||||
        onClick={() => setIsEditing(true)}
 | 
			
		||||
      >
 | 
			
		||||
        <ActionIcon icon={isEditing ? 'checkmark' : 'sketch'} />
 | 
			
		||||
        <ActionIcon icon="sketch" />
 | 
			
		||||
      </button>
 | 
			
		||||
    </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>
 | 
			
		||||
    )
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,6 @@
 | 
			
		||||
import {
 | 
			
		||||
  NetworkHealthState,
 | 
			
		||||
  useNetworkStatus,
 | 
			
		||||
} from 'components/NetworkHealthIndicator'
 | 
			
		||||
import { useAppState } from 'AppState'
 | 
			
		||||
import { NetworkHealthState, useNetworkStatus } from 'hooks/useNetworkStatus'
 | 
			
		||||
import { useKclContext } from 'lang/KclProvider'
 | 
			
		||||
import { useStore } from 'useStore'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Custom hook to determine if modeling actions should be disabled
 | 
			
		||||
@ -13,9 +10,7 @@ import { useStore } from 'useStore'
 | 
			
		||||
export function useShouldDisableModelingActions() {
 | 
			
		||||
  const { overallState } = useNetworkStatus()
 | 
			
		||||
  const { isExecuting } = useKclContext()
 | 
			
		||||
  const { isStreamReady } = useStore((s) => ({
 | 
			
		||||
    isStreamReady: s.isStreamReady,
 | 
			
		||||
  }))
 | 
			
		||||
  const { isStreamReady } = useAppState()
 | 
			
		||||
 | 
			
		||||
  return overallState !== NetworkHealthState.Ok || isExecuting || !isStreamReady
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -61,4 +61,5 @@ export const KCL_DEFAULT_LENGTH = `5`
 | 
			
		||||
export const KEYBINDING_CATEGORIES = {
 | 
			
		||||
  MODELING: 'modeling',
 | 
			
		||||
  COMMAND_BAR: 'command-bar',
 | 
			
		||||
  USER_INTERFACE: 'user-interface',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -6,15 +6,20 @@ export type InteractionMapItem = {
 | 
			
		||||
  name: string
 | 
			
		||||
  title: string
 | 
			
		||||
  sequence: string
 | 
			
		||||
  guard: (e: MouseEvent | KeyboardEvent) => boolean
 | 
			
		||||
  guard?: (e: MouseEvent | KeyboardEvent) => boolean
 | 
			
		||||
  action: () => void
 | 
			
		||||
  ownerId: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function makeOverrideKey(interactionMapItem: InteractionMapItem) {
 | 
			
		||||
  return `${interactionMapItem.ownerId}.${interactionMapItem.name}`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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: {
 | 
			
		||||
    interactionMap: [] as InteractionMapItem[],
 | 
			
		||||
    overrides: {} as { [key: string]: string },
 | 
			
		||||
    currentSequence: '' as string,
 | 
			
		||||
  },
 | 
			
		||||
  predictableActionArguments: true,
 | 
			
		||||
@ -35,6 +40,7 @@ export const interactionMapMachine = createMachine({
 | 
			
		||||
      | { type: 'Update prefix matrix' }
 | 
			
		||||
      | { type: 'Add last interaction to sequence' }
 | 
			
		||||
      | { type: 'Clear sequence' }
 | 
			
		||||
      | { type: 'Update overrides'; data: { [key: string]: string } }
 | 
			
		||||
      | { type: 'Resolve hotkey by prefix'; data: MouseEvent | KeyboardEvent }
 | 
			
		||||
      | { 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'],
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user