@@ -2,23 +2,21 @@ import {
22 Button ,
33 Checkbox ,
44 CheckboxGroup ,
5- Collection ,
65 ComboBox ,
7- Header ,
86 Input ,
9- Key ,
107 Label ,
118 ListBox ,
129 ListBoxItem ,
1310 Popover ,
14- Section ,
11+ Tab ,
12+ TabList ,
13+ TabPanel ,
14+ Tabs ,
1515} from "react-aria-components" ;
16- import {
17- hid_usage_from_page_and_id ,
18- hid_usage_page_get_ids ,
19- } from "../hid-usages" ;
16+ import { hid_usage_page_get_ids , hid_usage_get_metadata } from "../hid-usages" ;
2017import { useCallback , useMemo } from "react" ;
2118import { ChevronDown } from "lucide-react" ;
19+
2220
2321export interface HidUsagePage {
2422 id : number ;
@@ -33,41 +31,6 @@ export interface HidUsagePickerProps {
3331 onValueChanged : ( value ?: number ) => void ;
3432}
3533
36- type UsageSectionProps = HidUsagePage ;
37-
38- const UsageSection = ( { id, min, max } : UsageSectionProps ) => {
39- const info = useMemo ( ( ) => hid_usage_page_get_ids ( id ) , [ id ] ) ;
40-
41- let usages = useMemo ( ( ) => {
42- let usages = info ?. UsageIds || [ ] ;
43- if ( max || min ) {
44- usages = usages . filter (
45- ( i ) =>
46- ( i . Id <= ( max || Number . MAX_SAFE_INTEGER ) && i . Id >= ( min || 0 ) ) ||
47- ( id === 7 && i . Id >= 0xe0 && i . Id <= 0xe7 )
48- ) ;
49- }
50-
51- return usages ;
52- } , [ id , min , max , info ] ) ;
53-
54- return (
55- < Section id = { id } >
56- < Header className = "text-base-content/50" > { info ?. Name } </ Header >
57- < Collection items = { usages } >
58- { ( i ) => (
59- < ListBoxItem
60- className = "rac-hover:bg-base-300 pl-3 relative rac-focus:bg-base-300 cursor-default select-none rac-selected:before:content-['✔'] before:absolute before:left-[0] before:top-[0]"
61- id = { hid_usage_from_page_and_id ( id , i . Id ) }
62- >
63- { i . Name }
64- </ ListBoxItem >
65- ) }
66- </ Collection >
67- </ Section >
68- ) ;
69- } ;
70-
7134enum Mods {
7235 LeftControl = 0x01 ,
7336 LeftShift = 0x02 ,
@@ -109,6 +72,156 @@ function mask_mods(value: number) {
10972 return value & ~ ( mods_to_flags ( all_mods ) << 24 ) ;
11073}
11174
75+ const HidUsageGrid = ( {
76+ value,
77+ onValueChanged,
78+ usagePages,
79+ } : HidUsagePickerProps ) => {
80+ type Usage = {
81+ Name : string ;
82+ Id : number ;
83+ pageName : string ;
84+ pageId : number ;
85+ } ;
86+ const allUsages = useMemo ( ( ) => {
87+ return usagePages . flatMap ( ( page ) => {
88+ const pageInfo = hid_usage_page_get_ids ( page . id ) ;
89+ if ( ! pageInfo ) {
90+ return [ ] ;
91+ }
92+
93+ let usages = pageInfo . UsageIds || [ ] ;
94+ if ( page . max || page . min ) {
95+ usages = usages . filter (
96+ ( i ) =>
97+ ( i . Id <= ( page . max || Number . MAX_SAFE_INTEGER ) &&
98+ i . Id >= ( page . min || 0 ) ) ||
99+ ( page . id === 7 && i . Id >= 0xe0 && i . Id <= 0xe7 )
100+ ) ;
101+ }
102+
103+ return usages . map ( ( usage ) => ( {
104+ ...usage ,
105+ pageId : page . id ,
106+ pageName : pageInfo . Name ,
107+ } ) ) ;
108+ } ) ;
109+ } , [ usagePages ] ) ;
110+
111+ const selectedKey = value !== undefined ? mask_mods ( value ) : null ;
112+
113+ const getButtonLabel = ( usage : Usage ) => {
114+ const metadata = hid_usage_get_metadata ( usage . pageId , usage . Id ) ;
115+ if ( metadata ?. med ) {
116+ return metadata . med ;
117+ }
118+ if ( metadata ?. short ) {
119+ return metadata . short ;
120+ }
121+
122+ if ( usage . pageName === "Keyboard/Keypad" ) {
123+ const match = usage . Name . match ( / ^ ( K e y b o a r d | K e y p a d ) ( \S + ) / ) ;
124+ if ( match && match [ 2 ] ) {
125+ return match [ 2 ] ;
126+ }
127+ }
128+ return usage . Name ;
129+ } ;
130+
131+ const categorizedUsages = useMemo ( ( ) => {
132+ const categories : Record < string , Usage [ ] > = { } ;
133+
134+ for ( const usage of allUsages ) {
135+ const metadata = hid_usage_get_metadata ( usage . pageId , usage . Id ) ;
136+ const category = metadata ?. category || "Other" ;
137+
138+ if ( ! categories [ category ] ) {
139+ categories [ category ] = [ ] ;
140+ }
141+ categories [ category ] . push ( usage ) ;
142+ }
143+
144+ return categories ;
145+ } , [ allUsages ] ) ;
146+
147+ const categoryOrder = [ "Basic" , "Numpad" , "Apps/Media/Special" , "ISO/JIS" , "Other" ] ;
148+ const sortedCategories = Object . keys ( categorizedUsages ) . sort ( ( a , b ) => {
149+ const indexA = categoryOrder . indexOf ( a ) ;
150+ const indexB = categoryOrder . indexOf ( b ) ;
151+ if ( indexA !== - 1 && indexB !== - 1 ) return indexA - indexB ;
152+ if ( indexA !== - 1 ) return - 1 ;
153+ if ( indexB !== - 1 ) return 1 ;
154+ return a . localeCompare ( b ) ;
155+ } ) ;
156+
157+ return (
158+ < Tabs className = "flex flex-col" >
159+ < TabList className = "flex border-b" >
160+ { sortedCategories . map ( ( category ) => (
161+ < Tab key = { category } id = { category } className = "px-4 py-2 cursor-default outline-none rac-selected:border-b-2 rac-selected:border-primary rac-focus-visible:ring-2 rac-focus-visible:ring-primary rounded-t-md" >
162+ { category }
163+ </ Tab >
164+ ) ) }
165+ </ TabList >
166+ { sortedCategories . map ( ( category ) => (
167+ < TabPanel
168+ key = { category }
169+ id = { category }
170+ className = "min-h-56 max-h-56 overflow-y-auto flex flex-wrap justify-start content-start gap-1 p-1 border border-t-0 rounded-b rac-focus-visible:ring-2 rac-focus-visible:ring-primary"
171+ >
172+ { category === "Other" ? (
173+ < ComboBox
174+ className = "w-full p-2"
175+ defaultItems = { categorizedUsages [ category ] }
176+ selectedKey = { selectedKey }
177+ onSelectionChange = { ( key ) =>
178+ key !== null && onValueChanged ( key as number )
179+ }
180+ >
181+ < Label className = "text-sm" > Search for another key</ Label >
182+ < div className = "relative flex items-center" >
183+ < Input className = "p-1 rounded-l" />
184+ < Button className = "rounded-r bg-primary text-primary-content w-8 h-8 flex justify-center items-center" >
185+ < ChevronDown className = "size-4" />
186+ </ Button >
187+ </ div >
188+ < Popover className = "w-[var(--trigger-width)] max-h-4 shadow-md text-base-content rounded border-base-content bg-base-100" >
189+ < ListBox className = "block max-h-[30vh] min-h-[unset] overflow-auto p-2" >
190+ { ( item : Usage ) => {
191+ const usageValue = ( item . pageId << 16 ) | item . Id ;
192+ return (
193+ < ListBoxItem
194+ id = { usageValue }
195+ textValue = { item . Name }
196+ className = "rac-hover:bg-base-300 pl-3 relative rac-focus:bg-base-300 cursor-default select-none rac-selected:before:content-['✔'] before:absolute before:left-[0] before:top-[0]"
197+ >
198+ { item . Name }
199+ </ ListBoxItem >
200+ ) ;
201+ } }
202+ </ ListBox >
203+ </ Popover >
204+ </ ComboBox >
205+ ) : (
206+ categorizedUsages [ category ] . map ( ( usage ) => {
207+ const usageValue = ( usage . pageId << 16 ) | usage . Id ;
208+ return (
209+ < Button
210+ key = { usageValue }
211+ onPress = { ( ) => onValueChanged ( usageValue ) }
212+ className = { `w-16 h-16 p-1 rounded border text-center flex items-center justify-center ${ selectedKey === usageValue ? "bg-primary text-primary-content" : "bg-base-200 hover:bg-base-300" } ` }
213+ >
214+ { getButtonLabel ( usage ) }
215+ </ Button >
216+ ) ;
217+ } )
218+ ) }
219+ </ TabPanel >
220+ ) ) }
221+ </ Tabs >
222+ ) ;
223+ } ;
224+
112225export const HidUsagePicker = ( {
113226 label,
114227 value,
@@ -122,7 +235,7 @@ export const HidUsagePicker = ({
122235 } , [ value ] ) ;
123236
124237 const selectionChanged = useCallback (
125- ( e : Key | null ) => {
238+ ( e : number | undefined ) => {
126239 let value = typeof e == "number" ? e : undefined ;
127240 if ( value !== undefined ) {
128241 let mod_flags = mods_to_flags ( mods . map ( ( m ) => parseInt ( m ) ) ) ;
@@ -148,45 +261,31 @@ export const HidUsagePicker = ({
148261 ) ;
149262
150263 return (
151- < div className = "flex gap-2 relative" >
152- { label && < Label id = "hid-usage-picker" > { label } :</ Label > }
153- < ComboBox
154- selectedKey = { value ? mask_mods ( value ) : null }
155- onSelectionChange = { selectionChanged }
156- aria-labelledby = "hid-usage-picker"
157- >
158- < div className = "flex" >
159- < Input className = "p-1 rounded-l" />
160- < Button className = "rounded-r bg-primary text-primary-content w-8 h-8 flex justify-center items-center" >
161- < ChevronDown className = "size-4" />
162- </ Button >
163- </ div >
164- < Popover className = "w-[var(--trigger-width)] max-h-4 shadow-md text-base-content rounded border-base-content bg-base-100" >
165- < ListBox
166- items = { usagePages }
167- className = "block max-h-[30vh] min-h-[unset] overflow-auto p-2"
168- selectionMode = "single"
169- >
170- { ( { id, min, max } ) => < UsageSection id = { id } min = { min } max = { max } /> }
171- </ ListBox >
172- </ Popover >
173- </ ComboBox >
174- < CheckboxGroup
175- aria-label = "Implicit Modifiers"
176- className = "grid grid-flow-col gap-x-px auto-cols-[minmax(min-content,1fr)] content-stretch divide-x rounded-md"
177- value = { mods }
178- onChange = { modifiersChanged }
179- >
180- { all_mods . map ( ( m ) => (
181- < Checkbox
182- key = { m }
183- value = { m . toLocaleString ( ) }
184- className = "text-nowrap cursor-pointer grid px-2 content-center justify-center rac-selected:bg-primary border-base-100 bg-base-300 hover:bg-base-100 first:rounded-s-md last:rounded-e-md rac-selected:text-primary-content"
185- >
186- { mod_labels [ m ] }
187- </ Checkbox >
188- ) ) }
189- </ CheckboxGroup >
264+ < div className = "flex flex-col gap-2 relative" >
265+ < div className = "flex gap-2 items-center" >
266+ { label && < Label id = "hid-usage-picker" > { label } :</ Label > }
267+ < CheckboxGroup
268+ aria-label = "Implicit Modifiers"
269+ className = "grid grid-flow-col gap-x-px auto-cols-[minmax(min-content,1fr)] content-stretch divide-x rounded-md"
270+ value = { mods }
271+ onChange = { modifiersChanged }
272+ >
273+ { all_mods . map ( ( m ) => (
274+ < Checkbox
275+ key = { m }
276+ value = { m . toLocaleString ( ) }
277+ className = "text-nowrap cursor-pointer grid px-2 content-center justify-center rac-selected:bg-primary border-base-100 bg-base-300 hover:bg-base-100 first:rounded-s-md last:rounded-e-md rac-selected:text-primary-content"
278+ >
279+ { mod_labels [ m ] }
280+ </ Checkbox >
281+ ) ) }
282+ </ CheckboxGroup >
283+ </ div >
284+ < HidUsageGrid
285+ value = { value }
286+ onValueChanged = { selectionChanged }
287+ usagePages = { usagePages }
288+ />
190289 </ div >
191290 ) ;
192291} ;
0 commit comments