diff --git a/changelog.json b/changelog.json index 89a7874..bfb8aea 100644 --- a/changelog.json +++ b/changelog.json @@ -10,7 +10,8 @@ "Yaesu console: the power slider goes to 200 W on the models that make it (FTDX101MP and the like), and follows the radio if it ever reports more than expected. The slider, not the rig, was the 100 W limit.", "Yaesu console: the NAR button (narrow IF filter) is shown only when the radio answers the command, and says what it is. A button that did nothing when pressed read as a fault in the rig.", "The frequency readout now steps the Hz digits under the mouse wheel too, not just the kHz ones — 100, 10 and 1 Hz. Zero-beating a CW signal is a few tens of Hz, and it was the one move the display would not make.", - "The split-pile-up chaser only appears in CW: the marker comes from a skimmer decoding a report, so on SSB or FT8 there is nothing for it to chase." + "The split-pile-up chaser only appears in CW: the marker comes from a skimmer decoding a report, so on SSB or FT8 there is nothing for it to chase.", + "Cluster: a single click now only fills the callsign, and a DOUBLE click works the spot — QSY, mode and the rest. Running down the list used to drag the radio along with every line looked at." ], "fr": [ "Quand TQSL refuse un envoi, le journal contient désormais l'enregistrement ADIF exact qui lui a été remis et l'emplacement de station demandé pour la signature. « No QSOs processed » recouvre plusieurs causes sans rapport et le fichier temporaire est supprimé dès que TQSL rend la main : la seule pièce à conviction utile était justement invisible.", @@ -20,7 +21,8 @@ "Console Yaesu : le curseur de puissance monte à 200 W sur les modèles qui la sortent (FTDX101MP et consorts), et suit la radio si elle annonce davantage. C'était le curseur, pas la radio, qui plafonnait à 100 W.", "Console Yaesu : le bouton NAR (filtre FI étroit) n'apparaît que si la radio répond à la commande, et indique ce qu'il fait. Un bouton sans effet passait pour une panne de la radio.", "L'affichage de fréquence fait maintenant défiler aussi les chiffres des Hz à la molette, pas seulement ceux des kHz — 100, 10 et 1 Hz. Se caler au zéro-beat sur un signal CW se joue à quelques dizaines de Hz, et c'était le seul geste que l'affichage refusait.", - "Le chasseur de pile-up en split n'apparaît qu'en CW : le marqueur vient d'un skimmer qui décode un report, donc en SSB ou en FT8 il n'a rien à chasser." + "Le chasseur de pile-up en split n'apparaît qu'en CW : le marqueur vient d'un skimmer qui décode un report, donc en SSB ou en FT8 il n'a rien à chasser.", + "Cluster : un clic simple ne remplit plus que l'indicatif, et le DOUBLE clic travaille le spot — QSY, mode et le reste. Parcourir la liste entraînait la radio à chaque ligne regardée." ] }, { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a8156bc..f411797 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3159,6 +3159,19 @@ export default function App() { // Clicking a spot (cluster grid or any band map): tune the rig, set the mode, // fill the call, pre-fill POTA, (re)start the recording. Shared so every spot // source behaves identically. + // Single click on a cluster line: fill the callsign, nothing else. + // + // Reading the cluster means running down dozens of lines, and while a single + // click WORKED the spot every pass dragged the radio with it — one stray click + // while looking took the operator off the station they were working. Looking + // and going are two different intentions, so they are now two gestures: click + // to look the call up, double-click to go there. + function handleSpotSelect(s: any) { + if (!s?.dx_call?.trim()) return; + onCallsignInput(s.dx_call, { force: true }); + applySpotPOTA((s as any).pota_ref); + } + function handleSpotClick(s: any) { const m = inferSpotMode(s.comment ?? '', s.freq_hz); // Reflect the spot's freq/band in the entry strip IMMEDIATELY (optimistic), @@ -6074,7 +6087,7 @@ export default function App() {
- +
{clusterShowFilters && renderClusterFilters()}
@@ -7752,6 +7765,7 @@ export default function App() { rows={rendered as any} spotStatus={spotStatus} onSpotClick={handleSpotClick} + onSpotSelect={handleSpotSelect} /> ); })()} diff --git a/frontend/src/components/ClusterGrid.tsx b/frontend/src/components/ClusterGrid.tsx index 8a494ce..40045c1 100644 --- a/frontend/src/components/ClusterGrid.tsx +++ b/frontend/src/components/ClusterGrid.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { AllCommunityModule, ModuleRegistry, - type ColDef, type ColumnState, type GridReadyEvent, type RowClickedEvent, + type ColDef, type ColumnState, type GridReadyEvent, type RowClickedEvent, type RowDoubleClickedEvent, } from 'ag-grid-community'; import { hamlogGridTheme } from '@/lib/gridTheme'; import { AgGridReact } from 'ag-grid-react'; @@ -75,7 +75,13 @@ export type SpotStatusEntry = { type Props = { rows: ClusterSpot[]; spotStatus: Record; + // A DOUBLE click works the spot: QSY, mode, callsign, the lot. onSpotClick?: (s: ClusterSpot) => void; + // A SINGLE click only fills the callsign. Reading the cluster means passing + // over dozens of lines, and every pass used to drag the radio with it — one + // stray click while looking took the operator off the station they were + // working. Looking and going are now two different gestures. + onSpotSelect?: (s: ClusterSpot) => void; }; const COL_STATE_KEY = 'hamlog.clusterColState.v1'; @@ -500,7 +506,7 @@ const GROUP_ORDER = ['Spot', 'Geo']; const CLG_GRP_KEYS: Record = { Spot: 'clg2.grpSpot', Geo: 'clg2.grpGeo' }; const groupLabel = (t: TFn, g: string): string => t(CLG_GRP_KEYS[g] ?? g); -export function ClusterGrid({ rows, spotStatus, onSpotClick }: Props) { +export function ClusterGrid({ rows, spotStatus, onSpotClick, onSpotSelect }: Props) { const { t } = useI18n(); const gridRef = useRef(null); const [pickerOpen, setPickerOpen] = useState(false); @@ -583,6 +589,10 @@ export function ClusterGrid({ rows, spotStatus, onSpotClick }: Props) { }, []); function handleRowClicked(e: RowClickedEvent) { + if (e.data && onSpotSelect) onSpotSelect(e.data); + } + + function handleRowDoubleClicked(e: RowDoubleClickedEvent) { if (e.data && onSpotClick) onSpotClick(e.data); } @@ -647,6 +657,7 @@ export function ClusterGrid({ rows, spotStatus, onSpotClick }: Props) { onColumnVisible={saveColumnState} onSortChanged={saveColumnState} onRowClicked={handleRowClicked} + onRowDoubleClicked={handleRowDoubleClicked} animateRows={false} suppressCellFocus getRowId={(p) => `${(p.data as any).received_at}-${(p.data as any).dx_call}-${(p.data as any).source_id}`}