ADD: Action mapping

Including basic key capturing in renderer
This commit is contained in:
Robert Kossessa
2024-03-09 00:26:34 +01:00
parent f718363a85
commit 75efcb705c
8 changed files with 291 additions and 127 deletions

View File

@@ -7,7 +7,7 @@
@click="toggle = !toggle" @click="toggle = !toggle"
> >
<component :is="iconComponent" v-if="iconComponent" class="mr-2 size-4" /> <component :is="iconComponent" v-if="iconComponent" class="mr-2 size-4" />
<h2 class="py-4 text-sm">{{ title }}<slot name="title" /></h2> <h2 class="flex flex-1 items-center py-4 text-sm">{{ title }}<slot name="title" /></h2>
<Switch <Switch
v-if="showToggle" v-if="showToggle"
:checked="toggle" :checked="toggle"

View File

@@ -0,0 +1,105 @@
<template>
<div class="overflow-hidden rounded-lg border border-zinc-800 bg-zinc-900/50">
<div class="p-4">
<span class="font-mono text-sm text-muted-foreground"
>Action{{ actionIndex ? ` ${actionIndex}` : '' }}:</span
>
<span class="float-end mx-2 w-4 cursor-grab">
<GripHorizontal class="action-handle mb-0.5 inline-block size-4 text-zinc-600" />
</span>
<Popover v-model:open="open">
<PopoverTrigger as-child>
<Button
ref="comboboxButton"
variant="outline"
role="combobox"
:aria-expanded="open"
class="my-2 w-full justify-between"
>
<ScrambleText :text="value ? actionOptions[value].label : 'Select an action...'" />
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent class="p-0" :style="{ width: `${width * 1.125}px` }">
<Command>
<CommandInput class="h-9" placeholder="Search actions..." />
<CommandEmpty>
<ScrambleText scramble-on-mount text="No actions found." />
</CommandEmpty>
<CommandList>
<CommandGroup>
<CommandItem
v-for="(action, key) in actionOptions"
:key="key"
:value="action"
@select="
() => {
value = key
open = false
}
"
>
{{ action.label }}
<Check
:class="cn('ml-auto h-4 w-4', value === key ? 'opacity-100' : 'opacity-0')"
/>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<Separator />
<component :is="actionOptions[value]?.component ? actionOptions[value]?.component : WIP" />
</div>
</template>
<script setup lang="ts">
import WIP from '@renderer/components/WIP.vue'
import { Popover, PopoverTrigger, PopoverContent } from '@renderer/components/ui/popover'
import { Button } from '@renderer/components/ui/button'
import { Separator } from '@renderer/components/ui/separator'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList
} from '@renderer/components/ui/command'
import { ref } from 'vue'
import { cn } from '@renderer/lib/utils'
import SendKeyAction from '@renderer/components/config/actions/SendKeyAction.vue'
import SendStringAction from '@renderer/components/config/actions/SendStringAction.vue'
import ScrambleText from '@renderer/components/common/ScrambleText.vue'
import { ChevronsUpDown, Check, GripHorizontal } from 'lucide-vue-next'
import { useElementSize } from '@vueuse/core'
defineProps({
actionIndex: {
type: Number,
required: false
}
})
const actionOptions = ref({
sendKey: { label: 'Press Key or Combination', component: SendKeyAction },
sendString: { label: 'Type a String', component: SendStringAction },
sendMouse: { label: 'Move, Scroll or Click', component: 'SendMouseAction' },
sendGamepad: { label: 'Send a Gamepad Input', component: 'SendGamepadAction' },
sendMidi: { label: 'Send a MIDI Message', component: 'SendMidiAction' },
sendOsc: { label: 'Send an OSC Message', component: 'SendOscAction' },
sendSerial: { label: 'Send a Serial Message', component: 'SendSerialAction' },
controlMedia: { label: 'Control Media Playback', component: 'ControlMediaAction' },
controlSystem: { label: 'Control your OS', component: 'ControlSystemAction' },
runProgram: { label: 'Start a Program', component: 'RunProgramAction' },
changeProfile: { label: 'Change Device Profile', component: 'ChangeProfileAction' }
})
const comboboxButton = ref(null)
const { width } = useElementSize(comboboxButton)
const open = ref(false)
const value = ref('')
</script>

View File

@@ -0,0 +1,42 @@
<template>
<div class="flex flex-col gap-2">
<draggable
key="actionsDraggable"
class="flex flex-col gap-2"
group="keyActions"
item-key="id"
handle=".action-handle"
:list="actions"
v-bind="dragOptions"
>
<template #item="dragAction">
<div :key="dragAction.element.id">
<ActionCard :action-index="dragAction.index + 1" />
</div>
</template>
</draggable>
<button
class="flex flex-1 items-center justify-center rounded-lg border border-zinc-800 bg-zinc-900/50 p-2 text-sm text-muted-foreground hover:bg-zinc-800 hover:text-zinc-200"
>
<Plus class="mr-2" /> Add an action
</button>
</div>
</template>
<script setup lang="ts">
import { Plus } from 'lucide-vue-next'
import ActionCard from '@renderer/components/config/actions/ActionCard.vue'
import draggable from 'vuedraggable'
import { ref } from 'vue'
defineProps({
actions: {
type: Array,
required: true
}
})
const dragOptions = ref({
ghostClass: 'ghost',
animation: 150,
direction: 'vertical'
})
</script>

View File

@@ -0,0 +1,39 @@
<template>
<div class="flex flex-col p-4">
<Button
class="flex-1"
:class="{ 'bg-orange-600 hover:bg-orange-500': isCapturing }"
@click="toggleCapture"
>
{{ isCapturing ? 'Capturing Keyboard Input' : 'Capture Keyboard Input' }}
</Button>
<div class="mt-6 text-center font-mono text-sm text-muted-foreground">
Key: {{ lastEvent?.key }} | Code: {{ lastEvent?.keyCode }}
</div>
</div>
</template>
<script setup lang="ts">
import { Button } from '@renderer/components/ui/button'
import { ref, Ref } from 'vue'
const isCapturing = ref(false)
const keydownListener = (e: KeyboardEvent) => {
lastEvent.value = e
}
const keyupListener = (e: KeyboardEvent) => {
lastEvent.value = e
}
const toggleCapture = () => {
isCapturing.value = !isCapturing.value
// TODO: Do this in the main process
if (isCapturing.value) {
window.addEventListener('keydown', keydownListener)
window.addEventListener('keyup', keyupListener)
} else {
window.removeEventListener('keydown', keydownListener)
window.removeEventListener('keyup', keyupListener)
}
}
const lastEvent: Ref<KeyboardEvent | null> = ref(null)
</script>

View File

@@ -0,0 +1,8 @@
<template>
<div class="p-4">
<Input type="text" placeholder="String to be typed" />
</div>
</template>
<script setup lang="ts">
import { Input } from '@renderer/components/ui/input'
</script>

View File

@@ -1,92 +1,62 @@
<template> <template>
<ConfigSection title="Key Mapping" :icon-component="PlusSquare"> <ConfigSection :title="`${store.selectedKey} Pressed`" :icon-component="PanelBottomClose">
<template #title <template #title>
><span class="text-zinc-500"> ({{ store.selectedKey }})</span></template <span class="text-zinc-500">&nbsp;({{ actionsPressed.length }})</span></template
> >
<div class="my-4 px-8"> <ActionGroup :actions="actionsPressed" class="p-2" />
<span class="font-mono text-sm text-muted-foreground">Action:</span> </ConfigSection>
<Popover v-model:open="open"> <ConfigSection :title="`${store.selectedKey} Released`" :icon-component="PanelBottomOpen">
<PopoverTrigger as-child> <template #title>
<Button <span class="text-zinc-500">&nbsp;({{ actionsReleased.length }})</span></template
ref="comboboxButton" >
variant="outline" <ActionGroup :actions="actionsReleased" class="p-2" />
role="combobox" </ConfigSection>
:aria-expanded="open" <ConfigSection :title="`${store.selectedKey} Held`" :icon-component="Clock2">
class="my-2 w-full justify-between" <template #title>
> <span class="text-zinc-500">&nbsp;({{ actionsHeld.length }})</span></template
<ScrambleText :text="value ? actionOptions[value] : 'Select an action...'" /> >
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" /> <ActionGroup :actions="actionsHeld" class="p-2" />
</Button>
</PopoverTrigger>
<PopoverContent class="p-0" :style="{ width: $refs.comboboxButton?.$el.offsetWidth }">
<Command>
<CommandInput class="h-9" placeholder="Search actions..." />
<CommandEmpty>
<ScrambleText scramble-on-mount text="No actions found." />
</CommandEmpty>
<CommandList>
<CommandGroup>
<CommandItem
v-for="(action, key) in actionOptions"
:key="key"
:value="action"
@select="
() => {
value = key
open = false
}
"
>
{{ action }}
<Check
:class="cn('ml-auto h-4 w-4', value === key ? 'opacity-100' : 'opacity-0')"
/>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<WIP />
</ConfigSection> </ConfigSection>
</template> </template>
<script setup> <script setup>
import { PlusSquare, ChevronsUpDown, Check } from 'lucide-vue-next' import { PanelBottomClose, PanelBottomOpen, Clock2 } from 'lucide-vue-next'
import ConfigSection from '@renderer/components/common/ConfigSection.vue' import ConfigSection from '@renderer/components/common/ConfigSection.vue'
import WIP from '@renderer/components/WIP.vue'
import { Popover, PopoverTrigger, PopoverContent } from '@renderer/components/ui/popover'
import { Button } from '@renderer/components/ui/button'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList
} from '@renderer/components/ui/command'
import { ref } from 'vue'
import { cn } from '@renderer/lib/utils'
import ScrambleText from '@renderer/components/common/ScrambleText.vue'
import { useStore } from '@renderer/store' import { useStore } from '@renderer/store'
import { ref } from 'vue'
import ActionGroup from '../actions/ActionGroup.vue'
const store = useStore() const store = useStore()
const actionsPressed = ref([
const actionOptions = ref({ {
sendKey: 'Press Key or Combination', id: '1'
sendString: 'Type a String', },
sendMouse: 'Move, Scroll or Click', {
sendGamepad: 'Send a Gamepad Input', id: '2'
sendMidi: 'Send a MIDI Message', },
sendOsc: 'Send an OSC Message', {
sendSerial: 'Send a Serial Message', id: '3'
controlMedia: 'Control Media Playback', }
controlSystem: 'Control your OS', ])
runProgram: 'Start a Program' const actionsReleased = ref([
}) {
id: '4'
const comboboxButton = ref(null) },
{
const open = ref(false) id: '5'
const value = ref('') },
{
id: '6'
}
])
const actionsHeld = ref([
{
id: '7'
},
{
id: '8'
},
{
id: '9'
}
])
</script> </script>

View File

@@ -3,7 +3,7 @@
class="m-2 flex h-12 overflow-hidden rounded-lg transition-all" class="m-2 flex h-12 overflow-hidden rounded-lg transition-all"
:class="{ :class="{
'border border-zinc-100 bg-zinc-300': selected, 'border border-zinc-100 bg-zinc-300': selected,
'border border-transparent bg-zinc-900/30 hover:border-zinc-900': !selected, 'border border-zinc-800/50 bg-zinc-900/30': !selected,
group: showHoverButtons group: showHoverButtons
}" }"
> >
@@ -26,7 +26,7 @@
class="h-full min-w-0 flex-1 rounded-lg bg-transparent pl-8 text-sm transition-all focus-visible:outline-none focus-visible:ring-0" class="h-full min-w-0 flex-1 rounded-lg bg-transparent pl-8 text-sm transition-all focus-visible:outline-none focus-visible:ring-0"
:class="{ :class="{
'bg-zinc-300 font-semibold text-black hover:bg-zinc-200': selected, 'bg-zinc-300 font-semibold text-black hover:bg-zinc-200': selected,
'text-muted-foreground hover:bg-zinc-900': !selected 'text-muted-foreground hover:bg-zinc-800': !selected
}" }"
@blur="onNameInputBlur" @blur="onNameInputBlur"
/> />
@@ -35,7 +35,7 @@
type="submit" type="submit"
:class="{ :class="{
'bg-zinc-300 text-black hover:bg-zinc-200': selected, 'bg-zinc-300 text-black hover:bg-zinc-200': selected,
'text-zinc-100 hover:bg-zinc-900': !selected 'text-zinc-100 hover:bg-zinc-800': !selected
}" }"
class="flex aspect-square h-full shrink-0 items-center justify-center rounded-lg transition-all" class="flex aspect-square h-full shrink-0 items-center justify-center rounded-lg transition-all"
> >
@@ -47,7 +47,7 @@
v-else v-else
:class="{ :class="{
'bg-zinc-300 font-semibold text-black hover:bg-zinc-200': selected, 'bg-zinc-300 font-semibold text-black hover:bg-zinc-200': selected,
'text-muted-foreground hover:bg-zinc-900': !selected 'text-muted-foreground hover:bg-zinc-800': !selected
}" }"
class="flex-1 truncate rounded-lg pr-4 text-left text-sm transition-all" class="flex-1 truncate rounded-lg pr-4 text-left text-sm transition-all"
@click="!editing && $emit('select') && $refs.profileTitle.scramble()" @click="!editing && $emit('select') && $refs.profileTitle.scramble()"
@@ -56,7 +56,7 @@
<GripHorizontal <GripHorizontal
v-if="draggable" v-if="draggable"
:class="{ 'text-zinc-600': selected, 'text-muted-foreground': !selected }" :class="{ 'text-zinc-600': selected, 'text-muted-foreground': !selected }"
class="mb-0.5 inline-block size-4 opacity-0 transition-all group-hover:opacity-100" class="profile-handle mb-0.5 inline-block size-4 opacity-0 transition-all group-hover:opacity-100"
/> />
</span> </span>
<ScrambleText <ScrambleText
@@ -74,7 +74,7 @@
v-if="nameEditable" v-if="nameEditable"
:class="{ :class="{
'bg-zinc-300 text-black hover:bg-zinc-200': selected, 'bg-zinc-300 text-black hover:bg-zinc-200': selected,
'text-zinc-100 hover:bg-zinc-900': !selected, 'text-zinc-100 hover:bg-zinc-800': !selected,
'group-focus-within:w-12 group-hover:w-12': !editing 'group-focus-within:w-12 group-hover:w-12': !editing
}" }"
class="flex w-0 shrink-0 items-center justify-center rounded-lg transition-all" class="flex w-0 shrink-0 items-center justify-center rounded-lg transition-all"
@@ -85,7 +85,7 @@
<button <button
:class="{ :class="{
'bg-zinc-300 text-black hover:bg-zinc-200': selected, 'bg-zinc-300 text-black hover:bg-zinc-200': selected,
'text-zinc-100 hover:bg-zinc-900': !selected, 'text-zinc-100 hover:bg-zinc-800': !selected,
'group-focus-within:w-12 group-hover:w-12': !editing, 'group-focus-within:w-12 group-hover:w-12': !editing,
'rounded-l-lg': !nameEditable 'rounded-l-lg': !nameEditable
}" }"
@@ -121,7 +121,7 @@
<button <button
:class="{ :class="{
'bg-zinc-300 text-black hover:bg-zinc-200': selected, 'bg-zinc-300 text-black hover:bg-zinc-200': selected,
'text-zinc-100 hover:bg-zinc-900': !selected, 'text-zinc-100 hover:bg-zinc-800': !selected,
'group-focus-within:w-12 group-hover:w-12': !editing 'group-focus-within:w-12 group-hover:w-12': !editing
}" }"
class="flex w-0 shrink-0 items-center justify-center rounded-lg transition-all" class="flex w-0 shrink-0 items-center justify-center rounded-lg transition-all"

View File

@@ -61,6 +61,7 @@
item-key="name" item-key="name"
:list="store.profileCategories" :list="store.profileCategories"
v-bind="dragOptions" v-bind="dragOptions"
handle=".category-handle"
@start="drag = true" @start="drag = true"
@end="drag = false" @end="drag = false"
@change="onCategoryDrop" @change="onCategoryDrop"
@@ -79,45 +80,44 @@
({{ dragCategory.element.profiles?.length || 0 }})</span ({{ dragCategory.element.profiles?.length || 0 }})</span
> >
<span class="float-right mx-4 w-4 cursor-grab text-zinc-600"> <span class="float-right mx-4 w-4 cursor-grab text-zinc-600">
<GripHorizontal class="mb-0.5 inline-block size-4" /> <GripHorizontal class="category-handle mb-0.5 inline-block size-4" />
</span> </span>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
<TransitionGroup> <draggable
<draggable key="profilesDraggable"
key="profilesDraggable" group="profiles"
group="profiles" item-key="id"
item-key="id" :list="dragCategory.element.profiles"
:list="dragCategory.element.profiles" v-bind="dragOptions"
v-bind="dragOptions" handle=".profile-handle"
@start="drag = true" @start="drag = true"
@end="drag = false" @end="drag = false"
@change="(event) => onProfileDrop(event, dragCategory.index)" @change="(event) => onProfileDrop(event, dragCategory.index)"
> >
<template #header> <template #header>
<div class="hideable-header m-2 flex h-12 items-center justify-center"> <div class="hideable-header m-2 flex h-12 items-center justify-center">
<MoreHorizontal class="w-4 text-zinc-600" /> <MoreHorizontal class="w-4 text-zinc-600" />
</div> </div>
</template> </template>
<template #item="dragProfile"> <template #item="dragProfile">
<div :key="dragProfile.element.name"> <div :key="dragProfile.element.name">
<ProfileButton <ProfileButton
:profile="dragProfile.element" :profile="dragProfile.element"
:show-hover-buttons="!drag" :show-hover-buttons="!drag"
:selected="store.selectedProfile?.id === dragProfile.element.id" :selected="store.selectedProfile?.id === dragProfile.element.id"
@select=" @select="
() => { () => {
store.selectProfile(dragProfile.element.id) store.selectProfile(dragProfile.element.id)
showProfileConfig = true showProfileConfig = true
} }
" "
@duplicate="store.duplicateProfile(dragProfile.element.id)" @duplicate="store.duplicateProfile(dragProfile.element.id)"
@delete="store.removeProfile(dragProfile.element.id)" @delete="store.removeProfile(dragProfile.element.id)"
/> />
</div> </div>
</template> </template>
</draggable> </draggable>
</TransitionGroup>
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
</template> </template>