@@ -91,6 +91,192 @@ let mainMenuButton: HTMLButtonElement | null = null;
9191let mainMenuDropdown : HTMLDivElement | null = null ;
9292let mainMenuCloseTimeout : number | null = null ;
9393
94+ const MAIN_MENU_ACTIONS = [ 'connect' , 'settings' , 'status' , 'ifs' , 'pin-config' ] as const ;
95+ type MainMenuAction = typeof MAIN_MENU_ACTIONS [ number ] ;
96+
97+ const MAIN_MENU_ACTION_CHANNELS : Record < MainMenuAction , string > = {
98+ connect : 'open-printer-selection' ,
99+ settings : 'open-settings-window' ,
100+ status : 'open-status-dialog' ,
101+ ifs : 'open-ifs-dialog' ,
102+ 'pin-config' : 'shortcut-config:open'
103+ } ;
104+
105+ const MAIN_MENU_SHORTCUTS : Record < MainMenuAction , { key : string ; label : string } > = {
106+ connect : { key : 'k' , label : 'K' } ,
107+ settings : { key : ',' , label : ',' } ,
108+ status : { key : 'i' , label : 'I' } ,
109+ ifs : { key : 'm' , label : 'M' } ,
110+ 'pin-config' : { key : 'p' , label : 'P' }
111+ } ;
112+
113+ const TEXT_INPUT_TYPES = new Set ( [
114+ 'text' ,
115+ 'email' ,
116+ 'search' ,
117+ 'password' ,
118+ 'url' ,
119+ 'tel' ,
120+ 'number'
121+ ] ) ;
122+
123+ function isMainMenuAction ( action : string | null ) : action is MainMenuAction {
124+ return MAIN_MENU_ACTIONS . includes ( action as MainMenuAction ) ;
125+ }
126+
127+ class MenuShortcutManager {
128+ private initialized = false ;
129+ private isMac = false ;
130+ private enabledActions : Record < MainMenuAction , boolean > = {
131+ connect : true ,
132+ settings : true ,
133+ status : true ,
134+ ifs : false ,
135+ 'pin-config' : true
136+ } ;
137+
138+ initialize ( ) : void {
139+ this . isMac = window . PLATFORM === 'darwin' ;
140+ this . enabledActions . ifs = ifsMenuItemVisible ;
141+ this . updateShortcutLabels ( ) ;
142+
143+ if ( this . initialized ) {
144+ return ;
145+ }
146+
147+ document . addEventListener ( 'keydown' , this . handleKeydown ) ;
148+ this . initialized = true ;
149+ }
150+
151+ dispose ( ) : void {
152+ if ( ! this . initialized ) {
153+ return ;
154+ }
155+
156+ document . removeEventListener ( 'keydown' , this . handleKeydown ) ;
157+ this . initialized = false ;
158+ }
159+
160+ setActionEnabled ( action : MainMenuAction , enabled : boolean ) : void {
161+ this . enabledActions [ action ] = enabled ;
162+ }
163+
164+ updateShortcutLabels ( ) : void {
165+ const displayPrefix = this . isMac ? '⌘' : 'Ctrl+' ;
166+ const ariaPrefix = this . isMac ? 'Meta+' : 'Control+' ;
167+
168+ MAIN_MENU_ACTIONS . forEach ( ( action ) => {
169+ const config = MAIN_MENU_SHORTCUTS [ action ] ;
170+ const displayValue = this . isMac ? `${ displayPrefix } ${ config . label } ` : `${ displayPrefix } ${ config . label } ` ;
171+ const ariaValue = `${ ariaPrefix } ${ config . label } ` ;
172+
173+ const shortcutEl = document . querySelector < HTMLSpanElement > (
174+ `.menu-item-shortcut[data-shortcut-id="${ action } "]`
175+ ) ;
176+ if ( shortcutEl ) {
177+ shortcutEl . textContent = displayValue ;
178+ }
179+
180+ const button = document . querySelector < HTMLButtonElement > ( `.menu-item[data-action="${ action } "]` ) ;
181+ if ( button ) {
182+ button . setAttribute ( 'aria-keyshortcuts' , ariaValue ) ;
183+ }
184+ } ) ;
185+ }
186+
187+ private readonly handleKeydown = ( event : KeyboardEvent ) : void => {
188+ if ( ! this . initialized ) {
189+ return ;
190+ }
191+
192+ if ( event . defaultPrevented || event . repeat ) {
193+ return ;
194+ }
195+
196+ if ( ! this . isRelevantModifier ( event ) ) {
197+ return ;
198+ }
199+
200+ if ( event . altKey || event . shiftKey ) {
201+ return ;
202+ }
203+
204+ if ( this . isEditableContext ( ) ) {
205+ return ;
206+ }
207+
208+ const action = this . getActionFromEvent ( event ) ;
209+ if ( ! action || ! this . enabledActions [ action ] ) {
210+ return ;
211+ }
212+
213+ const channel = MAIN_MENU_ACTION_CHANNELS [ action ] ;
214+ if ( ! channel || ! window . api ?. send ) {
215+ return ;
216+ }
217+
218+ event . preventDefault ( ) ;
219+
220+ window . api . send ( channel ) ;
221+ closeMainMenu ( ) ;
222+ } ;
223+
224+ private isRelevantModifier ( event : KeyboardEvent ) : boolean {
225+ return this . isMac ? event . metaKey : event . ctrlKey ;
226+ }
227+
228+ private isEditableContext ( ) : boolean {
229+ const activeElement = document . activeElement ;
230+ if ( ! ( activeElement instanceof HTMLElement ) ) {
231+ return false ;
232+ }
233+
234+ if ( activeElement instanceof HTMLInputElement ) {
235+ if ( ! TEXT_INPUT_TYPES . has ( activeElement . type ) ) {
236+ return false ;
237+ }
238+
239+ return ! activeElement . readOnly && ! activeElement . disabled ;
240+ }
241+
242+ if ( activeElement instanceof HTMLTextAreaElement ) {
243+ return ! activeElement . readOnly && ! activeElement . disabled ;
244+ }
245+
246+ if ( activeElement instanceof HTMLSelectElement ) {
247+ return ! activeElement . disabled ;
248+ }
249+
250+ if ( activeElement . isContentEditable ) {
251+ return true ;
252+ }
253+
254+ return Boolean ( activeElement . closest ( '[contenteditable="true"]' ) ) ;
255+ }
256+
257+ private getActionFromEvent ( event : KeyboardEvent ) : MainMenuAction | null {
258+ const key = event . key . length === 1 ? event . key . toLowerCase ( ) : event . key ;
259+
260+ for ( const action of MAIN_MENU_ACTIONS ) {
261+ const shortcut = MAIN_MENU_SHORTCUTS [ action ] ;
262+ if ( shortcut . key === ',' ) {
263+ if ( event . key === ',' ) {
264+ return action ;
265+ }
266+ continue ;
267+ }
268+
269+ if ( key === shortcut . key ) {
270+ return action ;
271+ }
272+ }
273+
274+ return null ;
275+ }
276+ }
277+
278+ const menuShortcutManager = new MenuShortcutManager ( ) ;
279+
94280// Track filtration availability from backend
95281let filtrationAvailable = false ;
96282
@@ -1536,19 +1722,11 @@ function initializeMainMenu(): void {
15361722 toggleMainMenu ( ) ;
15371723 } ) ;
15381724
1539- const menuActions : Record < string , string > = {
1540- connect : 'open-printer-selection' ,
1541- settings : 'open-settings-window' ,
1542- status : 'open-status-dialog' ,
1543- ifs : 'open-ifs-dialog' ,
1544- 'pin-config' : 'shortcut-config:open'
1545- } ;
1546-
15471725 const menuItems = mainMenuDropdown . querySelectorAll < HTMLButtonElement > ( '.menu-item' ) ;
15481726 menuItems . forEach ( ( item ) => {
15491727 item . addEventListener ( 'click' , ( ) => {
15501728 const action = item . getAttribute ( 'data-action' ) ;
1551- const channel = action ? menuActions [ action ] : undefined ;
1729+ const channel = isMainMenuAction ( action ) ? MAIN_MENU_ACTION_CHANNELS [ action ] : undefined ;
15521730 if ( channel && window . api ?. send ) {
15531731 window . api . send ( channel ) ;
15541732 }
@@ -1739,6 +1917,8 @@ function updateIFSMenuItemVisibility(): void {
17391917 } else {
17401918 ifsMenuItem . classList . add ( 'hidden' ) ;
17411919 }
1920+
1921+ menuShortcutManager . setActionEnabled ( 'ifs' , ifsMenuItemVisible ) ;
17421922}
17431923
17441924/**
@@ -2038,6 +2218,7 @@ document.addEventListener('DOMContentLoaded', async () => {
20382218 setupLoadingEventListeners ( ) ;
20392219 initializeUI ( ) ;
20402220 initializeMainMenu ( ) ;
2221+ menuShortcutManager . initialize ( ) ;
20412222
20422223 // Initialize shortcut button system
20432224 initializeShortcutButtons ( ) ;
@@ -2092,6 +2273,7 @@ document.addEventListener('DOMContentLoaded', async () => {
20922273 */
20932274window . addEventListener ( 'beforeunload' , ( ) => {
20942275 console . log ( 'Cleaning up resources in enhanced renderer with GridStack and component system' ) ;
2276+ menuShortcutManager . dispose ( ) ;
20952277
20962278 // Clean up GridStack system
20972279 try {
0 commit comments