feat: persistent Ctrl+wheel zoom, saved award-column widths, F9 + hide-empty CW macros

- Zoom: our own Ctrl+wheel zoom (CSS zoom on the root, 50–250%), persisted in
  localStorage and restored at startup; Ctrl+0 resets. Replaces the non-persistent
  native WebView2 zoom. The freq-digit wheel now ignores Ctrl so it passes through
  to zoom.
- Award column widths: they were stripped from AG Grid's saved column state (that
  strip fixes a visibility desync) which also dropped their width. Now persisted
  separately per award code (localStorage + portable DB copy) and re-applied on
  rebuild/reopen.
- CW keyer widget: macros padded to 9 (F1–F9 slot) and empty macros hidden like
  the voice keyer, with the F-number kept tied to the real macro index so the
  F-key shortcuts still line up. New QRZ? default for F9.
This commit is contained in:
2026-07-25 13:14:05 +02:00
parent 6be0f43dd0
commit 91653bca57
6 changed files with 102 additions and 9 deletions
+28 -2
View File
@@ -231,7 +231,7 @@ function FreqWheelDisplay({ mhz, onNudge, className, placeholder = '—.——
{intPart}.
{khz.split('').map((d, i) => (
<span key={i}
onWheel={(e) => { e.preventDefault(); e.stopPropagation(); onNudge(e.deltaY < 0 ? stepHz[i] : -stepHz[i]); }}
onWheel={(e) => { if (e.ctrlKey || e.metaKey) return; e.preventDefault(); e.stopPropagation(); onNudge(e.deltaY < 0 ? stepHz[i] : -stepHz[i]); }}
className="cursor-ns-resize rounded-[2px] hover:bg-primary/25 transition-colors"
title="Scroll to change frequency">
{d}
@@ -843,6 +843,30 @@ export default function App() {
const [wkEsm, setWkEsm] = useState(false); // Enter-Sends-Message (N1MM-style CW flow)
const wkEsmRef = useRef(false);
useEffect(() => { wkEsmRef.current = wkEsm; }, [wkEsm]);
// Persistent Ctrl+wheel zoom. The native WebView2 Ctrl+wheel zoom isn't saved,
// so we run our own: CSS `zoom` on the root element, factor stored in
// localStorage and restored at startup. Ctrl+0 resets to 100%.
useEffect(() => {
const KEY = 'opslog.uiZoom';
let z = parseFloat(localStorage.getItem(KEY) || '1');
if (!Number.isFinite(z) || z <= 0) z = 1;
const apply = () => { (document.documentElement.style as any).zoom = String(z); };
apply();
const onWheel = (e: WheelEvent) => {
if (!e.ctrlKey && !e.metaKey) return; // plain wheel is left alone (scroll / freq nudge)
e.preventDefault();
z = Math.min(2.5, Math.max(0.5, Math.round((z + (e.deltaY < 0 ? 0.1 : -0.1)) * 10) / 10));
localStorage.setItem(KEY, String(z));
apply();
};
const onKey = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && (e.key === '0')) { e.preventDefault(); z = 1; localStorage.setItem(KEY, '1'); apply(); }
};
window.addEventListener('wheel', onWheel, { passive: false });
window.addEventListener('keydown', onKey);
return () => { window.removeEventListener('wheel', onWheel); window.removeEventListener('keydown', onKey); };
}, []);
// CW keyer output engine (persisted in the WinKeyer settings, chosen in
// Settings → CW Keyer): the WinKeyer hardware, or the Icom rig's own keyer via
// CI-V 0x17 (no extra hardware — sends over the CAT connection). Macros,
@@ -2237,7 +2261,9 @@ export default function App() {
setWkEnabled(!!s.enabled);
setWkPort(s.port ?? '');
setWkWpm(s.wpm ?? 25);
setWkMacros((s.macros ?? []) as WKMacro[]);
// Pad to 9 slots (F1F9) so an F9 macro always exists to fill; empty ones
// are hidden in the widget.
{ const mac = ((s.macros ?? []) as WKMacro[]).slice(); while (mac.length < 9) mac.push({ label: '', text: '' }); setWkMacros(mac); }
setWkEscClears(s.esc_clears_call !== false);
setWkSendOnType(!!s.send_on_type);
setWkEsm(!!s.esm);