Merge pull request #496 from Irev-Dev/keyboard-shortcuts

Initial keyboard shortcuts configuration implementation
This commit was merged in pull request #496.
This commit is contained in:
Frank Noirot
2021-09-09 18:12:59 -04:00
committed by GitHub
8 changed files with 2774 additions and 2586 deletions

View File

@@ -0,0 +1,68 @@
import { createContext, useContext } from 'react'
import { makeStyles } from '@material-ui/core/styles'
import Dialog from '@material-ui/core/Dialog'
import { editorMenuConfig } from './menuConfig'
const useStyles = makeStyles({
root: {
transform: `translate3d(0,0,50px)`,
},
})
interface ShortcutsModalContextType {
open: boolean
toggleOpen: () => any
}
export const ShortcutsModalContext = createContext<ShortcutsModalContextType>({
open: false,
toggleOpen: () => {},
})
export function useShortcutsModalContext() {
return useContext(ShortcutsModalContext)
}
const AllShortcutsModal = () => {
const classes = useStyles()
const { open, toggleOpen } = useShortcutsModalContext()
return (
<>
<Dialog
open={open}
onClose={() => toggleOpen()}
className={classes.root + ' bg-transparent'}
PaperProps={{
style: {
backgroundColor: 'transparent',
},
}}
>
<div className="bg-ch-gray-700 font-fira-sans shadow-lg text-ch-gray-300 p-4">
<h2 className="text-2xl mb-4">All Shortcuts</h2>
{editorMenuConfig
.filter((menu) => menu.items.length)
.map((menu) => (
<section key={'allshortcuts-' + menu.name} className="my-6">
<h3 className="text-xl border-b-2 pb-2 mb-2">{menu.label}</h3>
{menu.items.map((item) => (
<div
className="flex gap-16 justify-between"
key={'allshortcuts-' + menu.name + '-' + item.label}
>
<p>{item.label}</p>
<span className="text-right font-fira-code text-ch-gray-400">
{item.shortcutLabel}
</span>
</div>
))}
</section>
))}
</div>
</Dialog>
</>
)
}
export default AllShortcutsModal

View File

@@ -0,0 +1,71 @@
import { Menu } from '@headlessui/react'
import { useHotkeys } from 'react-hotkeys-hook'
export function DropdownItem({ config, state, thunkDispatch }) {
useHotkeys(config.shortcut, handleClick)
function handleClick(e) {
e.preventDefault()
config.callback(e, { state, thunkDispatch })
}
return (
<Menu.Item>
{({ active }) => (
<button
className={`${
active && 'bg-gray-600'
} px-2 py-1 flex justify-between`}
onClick={handleClick}
>
{config.label}
{config.shortcutLabel && (
<span className="text-gray-400 pl-6 text-right">
{config.shortcutLabel}
</span>
)}
</button>
)}
</Menu.Item>
)
}
export function Dropdown({
label,
disabled,
children,
}: {
label: string
disabled: boolean
children: React.ReactNode
}) {
return (
<div className="relative">
<Menu>
{({ open }) => (
<>
<Menu.Button
className={
'text-gray-100' +
(disabled ? ' text-gray-400 cursor-not-allowed' : '')
}
disabled={disabled}
>
{label}
</Menu.Button>
{children && (
<Menu.Items
static
className={
(open ? '' : 'hidden ') +
'absolute flex flex-col mt-4 bg-ch-gray-760 rounded text-gray-100 overflow-hidden whitespace-nowrap border border-ch-gray-700'
}
>
{children}
</Menu.Items>
)}
</>
)}
</Menu>
</div>
)
}

View File

@@ -1,133 +1,51 @@
import { Menu } from '@headlessui/react'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import Svg from 'src/components/Svg/Svg'
import { useRender } from 'src/components/IdeWrapper/useRender'
import { makeStlDownloadHandler, PullTitleFromFirstLine } from './helpers'
import { useSaveCode } from 'src/components/IdeWrapper/useSaveCode'
import CadPackage from 'src/components/CadPackage/CadPackage'
import { editorMenuConfig } from './menuConfig'
import AllShortcutsModal from './AllShortcutsModal'
import { Dropdown } from './Dropdowns'
const EditorMenu = () => {
const handleRender = useRender()
const saveCode = useSaveCode()
const { state, thunkDispatch } = useIdeContext()
const onRender = () => {
handleRender()
saveCode({ code: state.code })
}
const handleStlDownload = makeStlDownloadHandler({
type: state.objectData?.type,
ideType: state.ideType,
geometry: state.objectData?.data,
quality: state.objectData?.quality,
fileName: PullTitleFromFirstLine(state.code || ''),
thunkDispatch,
})
return (
<div className="flex justify-between bg-ch-gray-760 text-gray-100">
<div className="flex items-center h-9 w-full cursor-grab">
<div className=" text-ch-gray-760 bg-ch-gray-300 cursor-grab px-1.5 h-full flex items-center">
<Svg name="drag-grid" className="w-4 p-px" />
</div>
<div className="grid grid-flow-col-dense gap-6 px-5">
<FileDropdown
handleRender={onRender}
handleStlDownload={handleStlDownload}
/>
<button className="cursor-not-allowed" disabled>
Edit
<>
<div className="flex justify-between bg-ch-gray-760 text-gray-100">
<div className="flex items-center h-9 w-full cursor-grab">
<div className=" text-ch-gray-760 bg-ch-gray-300 cursor-grab px-2 h-full flex items-center">
<Svg name="drag-grid" className="w-4 p-px" />
</div>
<div className="grid grid-flow-col-dense gap-6 px-5">
{editorMenuConfig.map((menu) => (
<Dropdown
label={menu.label}
disabled={menu.disabled}
key={menu.label + '-dropdown'}
>
{menu.items.map((itemConfig) => (
<itemConfig.component
state={state}
thunkDispatch={thunkDispatch}
config={itemConfig}
key={menu.label + '-' + itemConfig.label}
/>
))}
</Dropdown>
))}
</div>
<button
className="text-ch-gray-300 h-full cursor-not-allowed"
aria-label="editor settings"
disabled
>
<Svg name="gear" className="w-6 p-px" />
</button>
<ViewDropdown
handleLayoutReset={() => thunkDispatch({ type: 'resetLayout' })}
/>
</div>
<button
className="text-ch-gray-300 h-full cursor-not-allowed"
aria-label="editor settings"
disabled
>
<Svg name="gear" className="w-6 p-px" />
</button>
<CadPackage cadPackage={state.ideType} className="px-3" />
</div>
<CadPackage cadPackage={state.ideType} className="px-3" />
</div>
<AllShortcutsModal />
</>
)
}
export default EditorMenu
function FileDropdown({ handleRender, handleStlDownload }) {
return (
<Dropdown name="File">
<Menu.Item>
{({ active }) => (
<button
className={`${active && 'bg-gray-600'} px-2 py-1`}
onClick={handleRender}
>
Save &amp; Render{' '}
<span className="text-gray-400 pl-4">
{/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ? (
<>
<Svg
name="mac-cmd-key"
className="h-3 w-3 inline-block text-left"
/>
S
</>
) : (
'Ctrl S'
)}
</span>
</button>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<button
className={`${active && 'bg-gray-600'} px-2 py-1 text-left`}
onClick={handleStlDownload}
>
Download STL
</button>
)}
</Menu.Item>
</Dropdown>
)
}
function ViewDropdown({ handleLayoutReset }) {
return (
<Dropdown name="View">
<Menu.Item>
{({ active }) => (
<button
className={`${active && 'bg-gray-600'} px-2 py-1`}
onClick={handleLayoutReset}
>
Reset layout
</button>
)}
</Menu.Item>
</Dropdown>
)
}
function Dropdown({
name,
children,
}: {
name: string
children: React.ReactNode
}) {
return (
<div className="relative">
<Menu>
<Menu.Button className="text-gray-100">{name}</Menu.Button>
<Menu.Items className="absolute flex flex-col mt-4 bg-ch-gray-760 rounded text-gray-100 overflow-hidden whitespace-nowrap border border-ch-gray-700">
{children}
</Menu.Items>
</Menu>
</div>
)
}

View File

@@ -0,0 +1,109 @@
import React from 'react'
import { useRender } from 'src/components/IdeWrapper/useRender'
import { makeStlDownloadHandler, PullTitleFromFirstLine } from './helpers'
import { useSaveCode } from 'src/components/IdeWrapper/useSaveCode'
import { DropdownItem } from './Dropdowns'
import { useShortcutsModalContext } from './AllShortcutsModal'
export function cmdOrCtrl() {
return /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ? '⌘' : 'Ctrl'
}
const fileMenuConfig: EditorMenuConfig = {
name: 'file',
label: 'File',
disabled: false,
items: [
{
label: 'Save & Render',
shortcut: 'ctrl+s, command+s',
shortcutLabel: cmdOrCtrl() + ' S',
component: (props) => {
const { state, config } = props
const handleRender = useRender()
const saveCode = useSaveCode()
function onRender(e) {
e.preventDefault()
handleRender()
saveCode({ code: state.code })
}
config.callback = onRender
return <DropdownItem {...props} />
},
},
{
label: 'Download STL',
shortcut: 'ctrl+shift+d, command+shift+d',
shortcutLabel: cmdOrCtrl() + ' Shift D',
component: (props) => {
const { state, thunkDispatch, config } = props
const handleStlDownload = makeStlDownloadHandler({
type: state.objectData?.type,
ideType: state.ideType,
geometry: state.objectData?.data,
quality: state.objectData?.quality,
fileName: PullTitleFromFirstLine(state.code || ''),
thunkDispatch,
})
config.callback = handleStlDownload
return <DropdownItem {...props} />
},
},
],
}
const editMenuConfig = {
name: 'edit',
label: 'Edit',
disabled: true,
items: [],
}
const viewMenuConfig = {
name: 'view',
label: 'View',
disabled: false,
items: [
{
label: 'Reset layout',
shortcut: 'ctrl+shift+r',
shortcutLabel: 'Ctrl Shift R',
component: (props) => {
const { config, thunkDispatch } = props
config.callback = () => thunkDispatch({ type: 'resetLayout' })
return <DropdownItem {...props} />
},
},
{
label: 'All shortcuts',
shortcut: 'ctrl+shift+/',
shortcutLabel: 'Ctrl Shift /',
component: (props) => {
const { config } = props
const { toggleOpen } = useShortcutsModalContext()
config.callback = toggleOpen
return <DropdownItem {...props} />
},
},
],
}
export const editorMenuConfig = [fileMenuConfig, editMenuConfig, viewMenuConfig]
export interface EditorMenuItemConfig {
label: string
shortcut: string
shortcutLabel: React.ReactElement | string
component: (props: any) => React.ReactElement
}
export interface EditorMenuConfig {
name: string
label: string
disabled: boolean
items: Array<EditorMenuItemConfig>
}

View File

@@ -8,6 +8,7 @@ import Svg from 'src/components/Svg/Svg'
import { useIdeInit } from 'src/components/EncodedUrl/helpers'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import { useSaveCode } from 'src/components/IdeWrapper/useSaveCode'
import { ShortcutsModalContext } from 'src/components/EditorMenu/AllShortcutsModal'
interface Props {
cadPackage: string
@@ -22,18 +23,25 @@ const IdeWrapper = ({ cadPackage }: Props) => {
saveCode({ code: state.code })
}
useIdeInit(cadPackage, project?.code || state?.code)
const [shortcutModalOpen, setShortcutModalOpen] = useState(false)
const shortcutModalContextValues = {
open: shortcutModalOpen,
toggleOpen: () => setShortcutModalOpen(!shortcutModalOpen),
}
return (
<div className="h-full flex">
<div className="w-14 bg-ch-gray-700 flex-shrink-0">
<IdeSideBar />
</div>
<div className="h-full flex flex-grow flex-col">
<nav className="flex">
<IdeHeader handleRender={onRender} />
</nav>
<IdeContainer />
</div>
<ShortcutsModalContext.Provider value={shortcutModalContextValues}>
<div className="w-16 bg-ch-gray-700 flex-shrink-0">
<IdeSideBar />
</div>
<div className="h-full flex flex-grow flex-col">
<nav className="flex">
<IdeHeader handleRender={onRender} />
</nav>
<IdeContainer />
</div>
</ShortcutsModalContext.Provider>
</div>
)
}