feat: wheel over RST; PSK Reporter keeps the band's window

The mouse wheel steps the RST fields — one S-unit on an RST or RS, one
decibel on a digital report, up for a better one — in the entry strip and
in the QSO editor. The dropdown beside them lists the reports worth
having to hand, not all 41 decibels, so the wheel works on the value
rather than walking the list. The listener is native and non-passive:
React attaches onWheel passively, where preventDefault does nothing and
the panel scrolls away under the field being adjusted.

PSK Reporter, whole-band scope: clicking a decode reset the report count
to zero. The window there is the BAND's — every FTx report on it,
filtered by target only when the analysis is drawn — and it was emptied
on every target change, throwing away an hour of evidence at the moment
the operator asked the question it answers. The narrow scope still clears
it: there the window is one station's, and keeping it would answer the
new question with the old station's evidence.
This commit is contained in:
2026-09-06 13:58:25 +02:00
parent 8b59954ce0
commit b421da165a
7 changed files with 120 additions and 13 deletions
+7 -3
View File
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { Trash2, Search, Loader2, CalendarDays } from 'lucide-react';
import { LookupCallsign, LookupCallsignFresh, DXCCForCountry, GetAwardDefs, ComputeQSOAwardRefs, GetListsSettings, OpenExternalURL, SetOpsLogQSLReceived } from '../../wailsjs/go/main/App';
import { rstOptions, type RSTLists } from '@/lib/rst';
import { rstOptions, stepRST, type RSTLists } from '@/lib/rst';
import { AwardRefSelector } from '@/components/AwardRefSelector';
import { AdifExtrasEditor } from '@/components/AdifExtrasEditor';
import { applyAwardRefs } from '@/lib/awardRefs';
@@ -598,9 +598,13 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [], b
value={draft.callsign ?? ''} onChange={(e) => set('callsign', e.target.value)} />
</div>
<div className="flex flex-col w-20"><Label>S</Label>
<Combobox value={draft.rst_sent ?? ''} options={rstOptions(draft.mode ?? '', rstLists)} commitOnType onChange={(v) => set('rst_sent', v)} /></div>
<Combobox value={draft.rst_sent ?? ''} options={rstOptions(draft.mode ?? '', rstLists)} commitOnType
onChange={(v) => set('rst_sent', v)}
onWheelStep={(d) => set('rst_sent', stepRST(draft.rst_sent ?? '', d, draft.mode ?? ''))} /></div>
<div className="flex flex-col w-20"><Label>R</Label>
<Combobox value={draft.rst_rcvd ?? ''} options={rstOptions(draft.mode ?? '', rstLists)} commitOnType onChange={(v) => set('rst_rcvd', v)} /></div>
<Combobox value={draft.rst_rcvd ?? ''} options={rstOptions(draft.mode ?? '', rstLists)} commitOnType
onChange={(v) => set('rst_rcvd', v)}
onWheelStep={(d) => set('rst_rcvd', stepRST(draft.rst_rcvd ?? '', d, draft.mode ?? ''))} /></div>
<Button type="button" variant="outline" className="h-10" onClick={fetchLookup} disabled={looking}
title={t('qedit.fetchTitle')}>
{looking ? <Loader2 className="size-4 animate-spin" /> : <Search className="size-4" />} {t('qedit.fetch')}
+20 -1
View File
@@ -9,7 +9,7 @@ import { cn } from '@/lib/utils';
// can't hold a typo'd value that isn't in the list.
export function Combobox({
value, onChange, options, placeholder, className, allowFreeText = false, commitOnType = false,
showToggle = false,
showToggle = false, onWheelStep,
}: {
value: string;
onChange: (v: string) => void;
@@ -21,6 +21,10 @@ export function Combobox({
// fields read live by other actions — e.g. RST, so a CW macro sent without
// leaving the field uses the value just typed.
commitOnType?: boolean;
// Wheel over the field steps the value. The control cannot know what a step
// means — a decibel here, an S-unit there — so it reports the direction and
// the owner decides.
onWheelStep?: (dir: 1 | -1) => void;
// Draw a chevron that opens the full list on click.
//
// Without it this control is indistinguishable from a plain text box: it opens
@@ -42,6 +46,21 @@ export function Combobox({
// leave the list floating over the wrong row.
const [menuPos, setMenuPos] = useState({ top: 0, left: 0, width: 0 });
// A NATIVE, non-passive listener: React attaches onWheel passively, where
// preventDefault does nothing at all and the panel scrolls away under the
// field being adjusted.
useEffect(() => {
const el = ref.current;
if (!el || !onWheelStep) return;
const onWheel = (e: WheelEvent) => {
if (e.deltaY === 0) return;
e.preventDefault();
onWheelStep(e.deltaY < 0 ? 1 : -1); // up is a better report
};
el.addEventListener('wheel', onWheel, { passive: false });
return () => el.removeEventListener('wheel', onWheel);
}, [onWheelStep]);
useEffect(() => {
function onDoc(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);