diff --git a/src/App.jsx b/src/App.jsx index e5ad5c6a..9a7761df 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -5,7 +5,7 @@ import { useState, useEffect, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { SettingsPanel, DXFilterManager, PSKFilterManager } from './components'; +import { SettingsPanel, DXFilterManager, PSKFilterManager, KeybindingsPanel } from './components'; import DockableLayout from './layouts/DockableLayout.jsx'; import ClassicLayout from './layouts/ClassicLayout.jsx'; @@ -47,6 +47,7 @@ import useLocalInstall from './hooks/app/useLocalInstall'; import useVersionCheck from './hooks/app/useVersionCheck'; import WhatsNew from './components/WhatsNew.jsx'; import { initCtyLookup } from './utils/ctyLookup.js'; +import { getAllLayers } from './plugins/layerRegistry.js'; import ActivateFilterManager from './components/ActivateFilterManager.jsx'; // Load DXCC entity database on app startup (non-blocking) @@ -61,6 +62,7 @@ const App = () => { const [showSettings, setShowSettings] = useState(false); const [showDXFilters, setShowDXFilters] = useState(false); const [showPSKFilters, setShowPSKFilters] = useState(false); + const [showKeybindings, setShowKeybindings] = useState(false); const [showPotaFilters, setShowPotaFilters] = useState(false); const [showSotaFilters, setShowSotaFilters] = useState(false); const [showWwffFilters, setShowWwffFilters] = useState(false); @@ -98,6 +100,68 @@ const App = () => { } }, [configLoaded, config.callsign]); + + const layerShortcuts = useMemo(() => { + const layers = getAllLayers(); + const map = {}; + const used = new Set(); + + for (const layer of layers) { + const name = (layer.name || layer.id || '').toLowerCase(); + for (const char of name) { + if (/[a-z]/.test(char) && !used.has(char)) { + map[char] = layer.id; + used.add(char); + break; + } + } + } + return map; + }, []); + + const keybindingsList = useMemo(() => { + return Object.entries(layerShortcuts) + .map(([key, id]) => { + const layer = getAllLayers().find(l => l.id === id); + let name = layer?.name || layer?.id || id; + if (name?.startsWith('plugins.layers.')) { + name = t(name, name); + } + return { key: key.toUpperCase(), description: `Toggle ${name}` }; + }) + .sort((a, b) => a.key.localeCompare(b.key)); + }, [layerShortcuts, t]); + + useEffect(() => { + const handleKey = (e) => { + if ( + showSettings || showDXFilters || showPSKFilters || showKeybindings || + document.activeElement?.tagName === 'INPUT' || + document.activeElement?.tagName === 'TEXTAREA' || + document.activeElement?.tagName === 'SELECT' + ) return; + + if (e.key === '?') { + setShowKeybindings(v => !v); + e.preventDefault(); + return; + } + + const layerId = layerShortcuts[e.key.toLowerCase()]; + if (layerId && window.hamclockLayerControls) { + const isEnabled = window.hamclockLayerControls.layers?.find(l => l.id === layerId)?.enabled ?? false; + window.hamclockLayerControls.toggleLayer(layerId, !isEnabled); + e.preventDefault(); + } + }; + + document.addEventListener('keydown', handleKey); + return () => document.removeEventListener('keydown', handleKey); + }, [ + showSettings, showDXFilters, showPSKFilters, showKeybindings, + layerShortcuts // only real dependency + ]); + const handleResetLayout = useCallback(() => { resetLayout(); setLayoutResetKey((prev) => prev + 1); @@ -411,6 +475,7 @@ const App = () => { rightSidebarVisible, getGridTemplateColumns, scale, + keybindingsList, }; return ( @@ -461,6 +526,11 @@ const App = () => { isOpen={showPSKFilters} onClose={() => setShowPSKFilters(false)} /> + setShowKeybindings(false)} + keybindings={keybindingsList} + /> { const layoutRef = useRef(null); const [model, setModel] = useState(() => Model.fromJson(loadLayout())); @@ -377,6 +381,7 @@ export const DockableApp = ({ 'rig-control': { name: 'Rig Control', icon: '📻' }, 'on-air': { name: 'On Air', icon: '🔴' }, 'id-timer': { name: 'ID Timer', icon: '📢' }, + keybindings: { name: 'Keyboard Shortcuts', icon: '⌨️' }, }; }, [isLocalInstall]); @@ -866,6 +871,10 @@ export const DockableApp = ({ content = ; break; + case 'keybindings': + content = ; + break; + default: content = (
@@ -930,6 +939,7 @@ export const DockableApp = ({ dxLocked, handleToggleDxLock, panelZoom, + keybindingsList, ], ); diff --git a/src/components/KeybindingsPanel.jsx b/src/components/KeybindingsPanel.jsx new file mode 100644 index 00000000..e950de2f --- /dev/null +++ b/src/components/KeybindingsPanel.jsx @@ -0,0 +1,338 @@ +/** + * KeybindingsPanel Component + * Displays all current keybindings in a floating panel or dockable panel + */ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +export const KeybindingsPanel = ({ isOpen, onClose, keybindings, nodeId }) => { + const { t } = useTranslation(); + const isDocked = !!nodeId; + + // Handle escape key to close (only for modal mode) + React.useEffect(() => { + if (!isOpen || isDocked) return; + + const handleEscape = (e) => { + if (e.key === 'Escape') { + onClose(); + } + }; + + document.addEventListener('keydown', handleEscape); + return () => document.removeEventListener('keydown', handleEscape); + }, [isOpen, onClose, isDocked]); + + // Modal mode - only render if open + if (!isDocked && !isOpen) return null; + + // Docked mode - render as panel content + if (isDocked) { + return ( +
+
+ {t('keybindings.panel.description', 'Press the following keys to toggle map layers:')} +
+
+ {keybindings.map(({ key, description }) => ( +
+ + {key} + + + {description} + +
+ ))} +
+ + ? + + + {t('keybindings.panel.toggle', 'Toggle this help panel')} + +
+
+
+ ); + } + + // Modal mode - render as floating overlay + return ( +
+
e.stopPropagation()} + > + {/* Header */} +
+

+ ⌨ {t('keybindings.panel.title', 'KEYBOARD SHORTCUTS')} +

+ +
+ + {/* Content */} +
+
+ {t('keybindings.panel.description', 'Press the following keys to toggle map layers:')} +
+ + {/* Keybindings list - 2 column grid for better space usage */} +
+ {keybindings.map(({ key, description }) => ( +
+ + {key} + + + {description} + +
+ ))} + + {/* Special keybinding for help */} +
+ + ? + + + {t('keybindings.panel.toggle', 'Toggle this help panel')} + +
+
+ + {/* Footer note */} +
+ 💡 {t('keybindings.panel.note', 'Press ESC or click outside to close this panel')} +
+
+
+
+ ); +}; + +export default KeybindingsPanel; diff --git a/src/components/WorldMap.jsx b/src/components/WorldMap.jsx index 481a0696..53f58ed1 100644 --- a/src/components/WorldMap.jsx +++ b/src/components/WorldMap.jsx @@ -2435,4 +2435,4 @@ export const WorldMap = ({ ); }; -export default WorldMap; \ No newline at end of file +export default WorldMap; diff --git a/src/components/index.js b/src/components/index.js index db247d17..5796d232 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -19,6 +19,7 @@ export { LocationPanel } from './LocationPanel.jsx'; export { SettingsPanel } from './SettingsPanel.jsx'; export { DXFilterManager } from './DXFilterManager.jsx'; export { PSKFilterManager } from './PSKFilterManager.jsx'; +export { KeybindingsPanel } from './KeybindingsPanel.jsx'; export { ActivateFilterManager } from './ActivateFilterManager.jsx'; export { SolarPanel } from './SolarPanel.jsx'; export { PropagationPanel } from './PropagationPanel.jsx'; diff --git a/src/lang/en.json b/src/lang/en.json index 13cd733c..a74d0c3e 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -415,5 +415,9 @@ "app.mapControls.calls.hide": "Hide Calls", "app.mapControls.calls.show": "Show Calls", "app.legend.moon": "Moon", - "propagation.heatmap.tooltip.stoplight": "Switch to stoplight colors (green=good)" + "propagation.heatmap.tooltip.stoplight" + "keybindings.panel.title": "KEYBOARD SHORTCUTS", + "keybindings.panel.description": "Press the following keys to toggle map layers:", + "keybindings.panel.toggle": "Toggle this help panel", + "keybindings.panel.note": "Press ESC or click outside to close this panel" }