Stack the calls that answer a CQ, the way N1MM does
A CQ is answered by more than one station. N1MM lets the operator put the
others on a stack and work them one after another; this is that, with N1MM's
keys and macros.
Ctrl+Alt+G and {STACKANOTHER} put the call being typed on the stack and clear
the boxes. Alt+G brings the top call back. Alt+D drops it. {SOCALLSTACK} both
stacks and unstacks in one key: it swaps the typed call for the top one, stacks
it when the stack is empty, and takes the top call when the boxes are.
{LOGTHENPOP} and {LOGTHENNEXT} log the contact and bring the next caller in.
{CLRSTACK} empties it.
The rules are N1MM's, and CallStack and the entry actions hold them without a
window, so they are unit-tested: stacking only while running, nothing shorter
than two characters, no dupe, no frequency typed into the callsign box, a call
already on the stack moved rather than repeated, and a multiplier to the front.
{SOCALLSTACK} takes the stacked call off before it puts the typed one on, so a
multiplier just typed cannot come straight back.
Two places differ. N1MM refuses your own call only while the stack is empty;
it is never a station to work, so it is refused either way here. And the order
is multipliers first in every mode: N1MM's digital window offers first-in and
last-in as well, and there is no digital window here.
The stack window opens by itself with the first call and closes with the last,
or Window > Call Stack opens it. It does not take the keyboard when it opens,
because the operator is typing the next call. The next call out is ringed and
each is coloured by what working it would bring, as in every other window.
OperatingPosition.JudgeCall now answers what a call alone is worth, which the
stack window and the check window both need; it was private to the check
window before.
Driven under Xvfb: four calls stacked with Ctrl+Alt+G and listed in the window,
Alt+G bringing the top one into the boxes, Alt+D dropping the next.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
This commit is contained in:
17
src/Nonemm.App/Windows/CallStackWindow.axaml
Normal file
17
src/Nonemm.App/Windows/CallStackWindow.axaml
Normal file
@@ -0,0 +1,17 @@
|
||||
<local:RefreshableWindow xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:Nonemm.App.Windows"
|
||||
x:Class="Nonemm.App.Windows.CallStackWindow"
|
||||
Title="Call stack" Width="180" Height="220"
|
||||
ShowActivated="False">
|
||||
<DockPanel Margin="4">
|
||||
<Button DockPanel.Dock="Bottom" Name="ClearButton" Content="Clear stack" FontSize="11"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
|
||||
Margin="0,4,0,0" Click="OnClear" />
|
||||
<TextBlock DockPanel.Dock="Bottom" FontSize="10" Opacity="0.7" Margin="2,4,0,0"
|
||||
Text="double-click to move up, right-click to drop" TextWrapping="Wrap" />
|
||||
<ScrollViewer>
|
||||
<StackPanel Name="Calls" />
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</local:RefreshableWindow>
|
||||
87
src/Nonemm.App/Windows/CallStackWindow.axaml.cs
Normal file
87
src/Nonemm.App/Windows/CallStackWindow.axaml.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Nonemm.Session;
|
||||
|
||||
namespace Nonemm.App.Windows;
|
||||
|
||||
/// The calls waiting to be worked, next one at the top and ringed. Each is
|
||||
/// coloured by what working it would bring, the way every other window colours
|
||||
/// a station. Double-click one to bring it to the top, right-click to drop it.
|
||||
public sealed partial class CallStackWindow : RefreshableWindow
|
||||
{
|
||||
private readonly AppSession session;
|
||||
private readonly int radioNumber;
|
||||
|
||||
public CallStackWindow(AppSession session, int radioNumber = 1)
|
||||
{
|
||||
this.session = session;
|
||||
this.radioNumber = radioNumber;
|
||||
InitializeComponent();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private OperatingPosition? Radio =>
|
||||
session.Positions.FirstOrDefault(p => p.RadioNumber == radioNumber);
|
||||
|
||||
public override void Refresh()
|
||||
{
|
||||
Calls.Children.Clear();
|
||||
if (Radio is not { } position)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ClearButton.IsEnabled = !position.Stack.IsEmpty;
|
||||
foreach (string call in position.Stack.Calls)
|
||||
{
|
||||
Calls.Children.Add(Row(position, call, isNext: call == position.Stack.Top));
|
||||
}
|
||||
}
|
||||
|
||||
private Control Row(OperatingPosition position, string call, bool isNext)
|
||||
{
|
||||
TextBlock text = new()
|
||||
{
|
||||
Text = call,
|
||||
FontFamily = new FontFamily("monospace"),
|
||||
FontSize = 14,
|
||||
Foreground = Verdicts.Colour(position.JudgeCall(call)),
|
||||
};
|
||||
Border row = new()
|
||||
{
|
||||
CornerRadius = new CornerRadius(9),
|
||||
BorderThickness = new Thickness(1),
|
||||
BorderBrush = isNext ? text.Foreground : Brushes.Transparent,
|
||||
Padding = new Thickness(6, 1),
|
||||
Margin = new Thickness(0, 0, 0, 2),
|
||||
Cursor = new Cursor(StandardCursorType.Hand),
|
||||
Child = text,
|
||||
};
|
||||
row.PointerPressed += (_, e) =>
|
||||
{
|
||||
if (e.GetCurrentPoint(row).Properties.IsRightButtonPressed)
|
||||
{
|
||||
position.Stack.Remove(call);
|
||||
position.Session.NotifyChanged();
|
||||
}
|
||||
};
|
||||
row.DoubleTapped += (_, _) =>
|
||||
{
|
||||
position.Stack.MoveToTop(call);
|
||||
position.Session.NotifyChanged();
|
||||
};
|
||||
return row;
|
||||
}
|
||||
|
||||
private void OnClear(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Radio is not { } position)
|
||||
{
|
||||
return;
|
||||
}
|
||||
position.Stack.Clear();
|
||||
position.Session.NotifyChanged();
|
||||
}
|
||||
}
|
||||
@@ -211,9 +211,85 @@ public sealed partial class EntryWindow
|
||||
case MessageCommand.Otrsp:
|
||||
_ = session.Box?.SendCommandAsync(action.Argument);
|
||||
break;
|
||||
case MessageCommand.StackAnother:
|
||||
StackAnother();
|
||||
break;
|
||||
case MessageCommand.SwapWithStack:
|
||||
SwapWithStack();
|
||||
break;
|
||||
case MessageCommand.LogThenPop:
|
||||
LogThenPop();
|
||||
break;
|
||||
case MessageCommand.ClearStack:
|
||||
Logging.Stack.Clear();
|
||||
ShowCallStack();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// N1MM's {STACKANOTHER} and Ctrl+Alt+G.
|
||||
private void StackAnother()
|
||||
{
|
||||
if (Logging is null || !Logging.StackAnother())
|
||||
{
|
||||
return;
|
||||
}
|
||||
SyncBoxes();
|
||||
boxes[0].Focus();
|
||||
ShowCallStack();
|
||||
Status($"{Logging.Stack.Count} stacked");
|
||||
}
|
||||
|
||||
/// N1MM's Alt+G.
|
||||
private void GrabStacked()
|
||||
{
|
||||
if (Logging is null || !Logging.GrabStacked())
|
||||
{
|
||||
return;
|
||||
}
|
||||
SyncBoxes();
|
||||
boxes[0].Focus();
|
||||
ShowCallStack();
|
||||
}
|
||||
|
||||
/// N1MM's Alt+D: the call on top of the stack is dropped without being
|
||||
/// worked.
|
||||
private void DropStacked()
|
||||
{
|
||||
if (Logging is null || Logging.Stack.IsEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Status($"{Logging.Stack.Next()} dropped");
|
||||
ShowCallStack();
|
||||
}
|
||||
|
||||
/// N1MM's {SOCALLSTACK}.
|
||||
private void SwapWithStack()
|
||||
{
|
||||
if (Logging is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Logging.SwapWithStack();
|
||||
SyncBoxes();
|
||||
boxes[0].Focus();
|
||||
ShowCallStack();
|
||||
}
|
||||
|
||||
/// N1MM's {LOGTHENPOP} and {LOGTHENNEXT}: one key logs the contact and
|
||||
/// brings the next caller into the boxes, so a run does not stop between
|
||||
/// callers.
|
||||
private void LogThenPop()
|
||||
{
|
||||
if (Logging is null || !Logging.IsRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
LogContact();
|
||||
GrabStacked();
|
||||
}
|
||||
|
||||
private void Step(int hertz)
|
||||
{
|
||||
if (Logging is null)
|
||||
|
||||
@@ -353,6 +353,28 @@ public sealed partial class EntryWindow
|
||||
|
||||
private void OnShowLog(object? sender, RoutedEventArgs e) => Show(() => new LogWindow(session));
|
||||
|
||||
private void OnShowCallStack(object? sender, RoutedEventArgs e) =>
|
||||
Show(() => new CallStackWindow(session, radioNumber));
|
||||
|
||||
/// N1MM opens the stack window when the first call goes on it and closes it
|
||||
/// when the last one comes off.
|
||||
private void ShowCallStack()
|
||||
{
|
||||
if (Logging is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (Logging.Stack.IsEmpty)
|
||||
{
|
||||
if (openWindows.TryGetValue(typeof(CallStackWindow), out Window? open))
|
||||
{
|
||||
open.Close();
|
||||
}
|
||||
return;
|
||||
}
|
||||
Show(() => new CallStackWindow(session, radioNumber), activate: false).Refresh();
|
||||
}
|
||||
|
||||
private void OnShowCheck(object? sender, RoutedEventArgs e) => Show(() => new CheckWindow(session));
|
||||
|
||||
private void OnShowBandmap(object? sender, RoutedEventArgs e) => Show(() => new BandmapWindow(session, Tune));
|
||||
@@ -587,11 +609,16 @@ public sealed partial class EntryWindow
|
||||
}
|
||||
|
||||
/// The one window of that kind, opened if it was not open already.
|
||||
private T Show<T>(Func<T> create) where T : Window
|
||||
/// `activate` is false for a window that opens on its own while the
|
||||
/// operator is typing, so the keyboard stays in the entry boxes.
|
||||
private T Show<T>(Func<T> create, bool activate = true) where T : Window
|
||||
{
|
||||
if (openWindows.TryGetValue(typeof(T), out Window? existing))
|
||||
{
|
||||
existing.Activate();
|
||||
if (activate)
|
||||
{
|
||||
existing.Activate();
|
||||
}
|
||||
return (T)existing;
|
||||
}
|
||||
T window = create();
|
||||
|
||||
@@ -115,6 +115,7 @@
|
||||
<MenuItem Header="Wi_ndow">
|
||||
<MenuItem Header="Available Mult's and Q's" Click="OnShowAvailable" />
|
||||
<MenuItem Header="_Bandmap" Click="OnShowBandmap" />
|
||||
<MenuItem Header="Call Stack" Click="OnShowCallStack" />
|
||||
<MenuItem Header="Check" Click="OnShowCheck" />
|
||||
<MenuItem Header="Log" Click="OnShowLog" InputGesture="Ctrl+L" />
|
||||
<MenuItem Header="Score Summary" Click="OnShowScore" />
|
||||
|
||||
@@ -418,6 +418,19 @@ public sealed partial class EntryWindow : Window
|
||||
e.Handled = true;
|
||||
OnSpotIt(this, new RoutedEventArgs());
|
||||
break;
|
||||
case Key.G when e.KeyModifiers.HasFlag(KeyModifiers.Control)
|
||||
&& e.KeyModifiers.HasFlag(KeyModifiers.Alt):
|
||||
e.Handled = true;
|
||||
StackAnother();
|
||||
break;
|
||||
case Key.G when e.KeyModifiers.HasFlag(KeyModifiers.Alt):
|
||||
e.Handled = true;
|
||||
GrabStacked();
|
||||
break;
|
||||
case Key.D when e.KeyModifiers.HasFlag(KeyModifiers.Alt):
|
||||
e.Handled = true;
|
||||
DropStacked();
|
||||
break;
|
||||
case Key.OemQuestion when e.KeyModifiers.HasFlag(KeyModifiers.Control):
|
||||
e.Handled = true;
|
||||
ToggleRun();
|
||||
|
||||
Reference in New Issue
Block a user