Lay the digital window out the way N1MM does
The window now carries what N1MM's does. Across the top: the TX indicator, the Letters/Figs box showing the word under the pointer in the other Baudot case, and the MouseOver box showing the word itself. Down the left of the receive pane is the pause bar, green while text prints and yellow while it is held. Under the panes are the message buttons and the row of engine controls, in N1MM's order. Both of N1MM's window scroll types are here. The scrolling pane adds lines at the bottom; the non-scrolling pane keeps text where it was printed, underlines the line being printed to and walks it down the window, so a call does not move as the pointer reaches it. A callsign is checked before it is coloured: anything shaped like a call under the generic routine, only calls in the files that are ticked under the resources routine, and under the combination everything, with a call the files do not hold marked in yellow. The garbage text search finds a Master.scp call inside a run of noise. The mouse and the keys are N1MM's. Shift+click converts the case before the word is transferred, Ctrl+click puts it in the box the cursor is in, Alt+click puts a call on the stack, a double click replaces the call in the box. The receive pane and the grab list have their right click menus. Alt+T and Ctrl+K turn the transmitter on with the cursor in the transmit pane, Alt+G grabs, Esc aborts. New from the menus: hover mode, the callsign pause routines, sending a text file, a date-stamped copy of the receive pane in ExportFiles, clearing the grab list on a CQ or a QSY, the ham default and AFC off on a run to S&P change, AFC on with CQ, and raising the window with the entry window it belongs to. The grab list holds ten calls, as N1MM's does. The setup window is N1MM's dialog: a General / MMTTY Setup tab, an Engine tab for the Wine and bridge paths, and a Message Setup tab with the buttons laid out as they sit in the window, a caption and text box for the one being edited, the macro list, the count of buttons to show, and import and save of a .mc file. Right-clicking a message button opens that tab, so the free-form macro dialog is gone. The title carries the dial frequency with the mark tone added to it, or the dial frequency alone when the radio already reads out what it transmits. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
This commit is contained in:
@@ -3,6 +3,7 @@ using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.VisualTree;
|
||||
using Nonemm.Session;
|
||||
|
||||
namespace Nonemm.App.Windows;
|
||||
@@ -11,6 +12,10 @@ namespace Nonemm.App.Windows;
|
||||
/// transmit pane, and the clicking and hovering in the receive pane.
|
||||
public sealed partial class DigitalWindow
|
||||
{
|
||||
/// The call the pointer is resting on, which hover mode takes and the
|
||||
/// modifier keys act on.
|
||||
private string underPointer = "";
|
||||
|
||||
/// A callsign the modem decoded goes in the grab list, unless it is ours,
|
||||
/// the one already being typed, or the filter turns it away.
|
||||
private void Heard(string call)
|
||||
@@ -21,7 +26,7 @@ public sealed partial class DigitalWindow
|
||||
{
|
||||
return;
|
||||
}
|
||||
grab.NewestFirst = session.Settings.DigitalGrabNewestFirst;
|
||||
grab.NewestFirst = Settings.DigitalGrabNewestFirst;
|
||||
if (grab.Add(call))
|
||||
{
|
||||
ShowGrabList();
|
||||
@@ -29,7 +34,7 @@ public sealed partial class DigitalWindow
|
||||
}
|
||||
|
||||
private bool PassesFilter(OperatingPosition position, string call) =>
|
||||
session.Settings.DigitalGrabFilter.ToLowerInvariant() switch
|
||||
Settings.DigitalGrabFilter.ToLowerInvariant() switch
|
||||
{
|
||||
"notdupes" => position.JudgeCall(call)?.IsDupe != true,
|
||||
"knowncalls" => session.Calls.Holds(call) || position.Log.WorkedBefore(call).Count > 0,
|
||||
@@ -55,7 +60,7 @@ public sealed partial class DigitalWindow
|
||||
{
|
||||
Text = call,
|
||||
FontFamily = new FontFamily("monospace"),
|
||||
FontSize = session.Settings.DigitalFontSize,
|
||||
FontSize = Settings.DigitalFontSize,
|
||||
Foreground = Verdicts.Colour(position.JudgeCall(call)),
|
||||
};
|
||||
Border row = new()
|
||||
@@ -65,29 +70,59 @@ public sealed partial class DigitalWindow
|
||||
// a border with no background of its own is not clickable away from
|
||||
// the letters, and the whole row is the target
|
||||
Background = Brushes.Transparent,
|
||||
Tag = call,
|
||||
Child = text,
|
||||
};
|
||||
row.PointerPressed += (_, e) => GrabFromList(call, e);
|
||||
return row;
|
||||
}
|
||||
|
||||
/// Left click grabs the call, right click drops it from the list, which is
|
||||
/// what N1MM's grab window does.
|
||||
/// Left click grabs the call; the right click menu is on the list itself.
|
||||
private void GrabFromList(string call, PointerPressedEventArgs e)
|
||||
{
|
||||
if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed)
|
||||
if (!e.GetCurrentPoint(this).Properties.IsRightButtonPressed)
|
||||
{
|
||||
Grab(call);
|
||||
}
|
||||
}
|
||||
|
||||
/// N1MM's grab list menu: clear the whole list, clear the call under the
|
||||
/// pointer, or change the order calls come off it in.
|
||||
private void OnGrabListPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (!e.GetCurrentPoint(this).Properties.IsRightButtonPressed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
string call = (e.Source as Control)?.FindAncestorOfType<Border>()?.Tag as string ?? "";
|
||||
MenuFlyout menu = new();
|
||||
menu.Items.Add(Item("Clear list", () =>
|
||||
{
|
||||
grab.Clear();
|
||||
ShowGrabList();
|
||||
}));
|
||||
menu.Items.Add(Item($"Clear selected call{(call.Length > 0 ? $" ({call})" : "")}", () =>
|
||||
{
|
||||
grab.Remove(call);
|
||||
ShowGrabList();
|
||||
return;
|
||||
}
|
||||
Grab(call);
|
||||
}, call.Length > 0));
|
||||
menu.Items.Add(new Separator());
|
||||
menu.Items.Add(Item("Last in, first out", () => GrabOrder(newestFirst: true)));
|
||||
menu.Items.Add(Item("First in, first out", () => GrabOrder(newestFirst: false)));
|
||||
menu.ShowAt(GrabCalls, showAtPointer: true);
|
||||
}
|
||||
|
||||
private static MenuItem Item(string header, Action act, bool enabled = true)
|
||||
{
|
||||
MenuItem item = new() { Header = header, IsEnabled = enabled };
|
||||
item.Click += (_, _) => act();
|
||||
return item;
|
||||
}
|
||||
|
||||
/// Puts a call in the entry window. With call stacking on and a call
|
||||
/// already being typed, the one being typed goes on the stack first, so
|
||||
/// neither is lost — N1MM's digital call stacking.
|
||||
private void Grab(string call)
|
||||
private void Grab(string call, bool space = true)
|
||||
{
|
||||
if (Radio is not { } position)
|
||||
{
|
||||
@@ -101,7 +136,7 @@ public sealed partial class DigitalWindow
|
||||
}
|
||||
grab.Remove(call);
|
||||
ShowGrabList();
|
||||
entry.GrabCall(call, session.Settings.DigitalGrabSendsSpace);
|
||||
entry.GrabCall(call, space && Settings.DigitalGrabSendsSpace);
|
||||
entry.Refresh();
|
||||
}
|
||||
|
||||
@@ -114,6 +149,9 @@ public sealed partial class DigitalWindow
|
||||
}
|
||||
}
|
||||
|
||||
/// What the pointer rests on: the Letters/Figs and MouseOver boxes follow
|
||||
/// it, and with hover mode on a callsign goes to the entry window without a
|
||||
/// click at all.
|
||||
private void OnReceivePointerMoved(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (e.Source is not TextBlock block || block.Text is not { } text || text.Trim().Length == 0)
|
||||
@@ -128,7 +166,35 @@ public sealed partial class DigitalWindow
|
||||
Unhighlight();
|
||||
hovered = block;
|
||||
block.TextDecorations = TextDecorations.Underline;
|
||||
MouseOverText.Text = DigitalReceiver.Scrub(text);
|
||||
underPointer = DigitalReceiver.Scrub(text);
|
||||
MouseOverText.Text = underPointer;
|
||||
// the box is Baudot's, so it says nothing in a mode that is not RTTY,
|
||||
// which is what N1MM writes in it
|
||||
FigsText.Text = (Radio?.Mode ?? Nonemm.Core.Modes.Rtty) == Nonemm.Core.Modes.Rtty
|
||||
? Baudot.LettersFigs(underPointer)
|
||||
: "NOT USED";
|
||||
WhenPointerRestsOnCall(underPointer);
|
||||
}
|
||||
|
||||
/// N1MM's hover mode and its callsign pause routines, both worked off the
|
||||
/// call the pointer is resting on: the call goes into an empty callsign box
|
||||
/// on its own, and the pane stops so the text under the pointer stays where
|
||||
/// it is.
|
||||
private void WhenPointerRestsOnCall(string text)
|
||||
{
|
||||
if (Radio is not { } position || CallToTake(position, text) is not { Length: > 0 } call)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (Settings.DigitalPauseOnCallsign && position.Entry.Call.Trim().Length == 0)
|
||||
{
|
||||
paused = true;
|
||||
ShowPauseBar();
|
||||
}
|
||||
if (Settings.DigitalHoverMode && position.Entry.Call.Trim().Length == 0)
|
||||
{
|
||||
Grab(call, Settings.DigitalClickSendsSpace);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnReceivePointerExited(object? sender, PointerEventArgs e)
|
||||
@@ -144,49 +210,70 @@ public sealed partial class DigitalWindow
|
||||
hovered.TextDecorations = null;
|
||||
hovered = null;
|
||||
}
|
||||
underPointer = "";
|
||||
MouseOverText.Text = "";
|
||||
FigsText.Text = "";
|
||||
}
|
||||
|
||||
/// One left click copies the word under the pointer, the way N1MM does: a
|
||||
/// callsign goes in the callsign box, anything else in the exchange box the
|
||||
/// contest keeps for it. A click on nothing pauses the pane instead, so
|
||||
/// text can be read while more arrives. A right click presses Enter in the
|
||||
/// entry window when that option is on.
|
||||
/// contest keeps for it. Ctrl puts it in the box the cursor is in whatever
|
||||
/// it holds, shift converts it to the other Baudot case first, alt puts a
|
||||
/// call on the call stack, and a double click replaces a call already in
|
||||
/// the box. A right click presses Enter in the entry window when that
|
||||
/// option is on, and opens the menu when it is not.
|
||||
private void OnReceivePointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed)
|
||||
{
|
||||
RightClick();
|
||||
RightClick(e);
|
||||
return;
|
||||
}
|
||||
string clicked = e.Source is TextBlock block ? DigitalReceiver.Scrub(block.Text ?? "") : "";
|
||||
if (clicked.Length == 0)
|
||||
{
|
||||
paused = true;
|
||||
ShowPauseBar();
|
||||
return;
|
||||
}
|
||||
Resume();
|
||||
Copy(clicked);
|
||||
KeyModifiers keys = e.KeyModifiers;
|
||||
if (keys.HasFlag(KeyModifiers.Shift))
|
||||
{
|
||||
clicked = Baudot.LettersFigs(clicked);
|
||||
}
|
||||
if (keys.HasFlag(KeyModifiers.Control))
|
||||
{
|
||||
entry.GrabIntoFocused(DigitalElement.Trim(clicked));
|
||||
return;
|
||||
}
|
||||
Copy(clicked, keys.HasFlag(KeyModifiers.Alt), e.ClickCount > 1);
|
||||
}
|
||||
|
||||
/// Where a clicked word goes. N1MM's order: an empty callsign box takes a
|
||||
/// call, a callsign box that already holds one takes another only onto the
|
||||
/// call stack, and everything else is an exchange element.
|
||||
private void Copy(string clicked)
|
||||
/// call stack or on a double click, and everything else is an exchange
|
||||
/// element.
|
||||
private void Copy(string clicked, bool toStack, bool replaces)
|
||||
{
|
||||
if (Radio is not { } position)
|
||||
{
|
||||
return;
|
||||
}
|
||||
string call = DigitalReceiver.TrimToCall(clicked);
|
||||
bool isCall = call.Length is >= 3 and < 13
|
||||
&& !call.All(char.IsAsciiDigit)
|
||||
&& DigitalReceiver.IsCallsign(call)
|
||||
&& !string.Equals(call, position.Me.Callsign, StringComparison.OrdinalIgnoreCase);
|
||||
string call = CallToTake(position, clicked);
|
||||
bool waiting = position.Entry.Call.Trim().Length == 0;
|
||||
if (isCall && (waiting || StackingOrder() != StackOrder.Disabled))
|
||||
if (call.Length > 0 && toStack)
|
||||
{
|
||||
Grab(call);
|
||||
position.Stack.Order = StackingOrder();
|
||||
position.Stack.Add(call, position.JudgeCall(call)?.IsNewMultiplier == true);
|
||||
grab.Remove(call);
|
||||
ShowGrabList();
|
||||
entry.Refresh();
|
||||
return;
|
||||
}
|
||||
if (call.Length > 0 && (waiting || replaces || StackingOrder() != StackOrder.Disabled))
|
||||
{
|
||||
Grab(call, Settings.DigitalClickSendsSpace);
|
||||
return;
|
||||
}
|
||||
if (!waiting)
|
||||
@@ -195,18 +282,66 @@ public sealed partial class DigitalWindow
|
||||
}
|
||||
}
|
||||
|
||||
private void RightClick()
|
||||
/// The callsign in a clicked or hovered word, or empty when it is not one
|
||||
/// this station would work. Our own call is never taken.
|
||||
private string CallToTake(OperatingPosition position, string clicked)
|
||||
{
|
||||
if (!session.Settings.DigitalRightClickSendsEnter)
|
||||
string call = DigitalReceiver.TrimToCall(clicked);
|
||||
bool isCall = call.Length is >= 3 and < 13
|
||||
&& !call.All(char.IsAsciiDigit)
|
||||
&& DigitalReceiver.IsCallsign(call)
|
||||
&& !string.Equals(call, position.Me.Callsign, StringComparison.OrdinalIgnoreCase);
|
||||
return isCall ? call : "";
|
||||
}
|
||||
|
||||
private void RightClick(PointerPressedEventArgs e)
|
||||
{
|
||||
if (Settings.DigitalRightClickSendsEnter)
|
||||
{
|
||||
entry.PressEnter();
|
||||
if (!Settings.DigitalRightClickKeepsCall && Radio is { } position)
|
||||
{
|
||||
position.Wipe();
|
||||
entry.Refresh();
|
||||
}
|
||||
return;
|
||||
}
|
||||
entry.PressEnter();
|
||||
if (!session.Settings.DigitalRightClickKeepsCall && Radio is { } position)
|
||||
MenuFlyout menu = new();
|
||||
menu.Items.Add(Item("Clear RX", () => OnClearReceive(this, new RoutedEventArgs())));
|
||||
menu.Items.Add(Item("Output to text file", () =>
|
||||
Store(Settings with { DigitalOutputToTextFile = !Settings.DigitalOutputToTextFile })));
|
||||
menu.Items.Add(Item(paused ? "Let the pane run" : "Hold the pane still", () =>
|
||||
{
|
||||
position.Wipe();
|
||||
entry.Refresh();
|
||||
if (paused)
|
||||
{
|
||||
Resume();
|
||||
}
|
||||
else
|
||||
{
|
||||
paused = true;
|
||||
ShowPauseBar();
|
||||
}
|
||||
}));
|
||||
menu.ShowAt(ReceiveLines, showAtPointer: true);
|
||||
}
|
||||
|
||||
/// N1MM's pause bar: the strip beside the receive pane, green while text is
|
||||
/// printing and yellow while it is held.
|
||||
private void OnPauseBarPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (paused)
|
||||
{
|
||||
Resume();
|
||||
return;
|
||||
}
|
||||
paused = true;
|
||||
ShowPauseBar();
|
||||
}
|
||||
|
||||
private void ShowPauseBar()
|
||||
{
|
||||
PauseBar.Background = paused ? Brushes.Gold : new SolidColorBrush(Color.FromRgb(0x2E, 0x8B, 0x57));
|
||||
PauseText.Text = paused ? "the pane is held still" : "";
|
||||
}
|
||||
|
||||
/// Lets the pane scroll again and prints what arrived while it was stopped.
|
||||
@@ -217,6 +352,7 @@ public sealed partial class DigitalWindow
|
||||
return;
|
||||
}
|
||||
paused = false;
|
||||
ShowPauseBar();
|
||||
if (held.Length == 0)
|
||||
{
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user