Add CW keying, function key messages and the documentation

The function keys send through cwdaemon or a WinKeyer, with N1MM's message
macros. Escape stops sending.

Settings are read with the reflection serializer rather than a generated one:
the generated one hands back null for every property the file leaves out
instead of the value the property is declared with, which crashed the program
the first time a new setting was added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik
2026-08-27 11:23:30 +00:00
parent d96cbe146b
commit f5aa77ece3
25 changed files with 1002 additions and 26 deletions

View File

@@ -6,7 +6,7 @@
<StackPanel Margin="14" Spacing="6">
<Grid ColumnDefinitions="*,8,110" RowDefinitions="Auto,Auto">
<TextBlock Text="Node" FontSize="11" Opacity="0.7" Margin="0,0,0,1" />
<TextBox Name="HostBox" Grid.Row="1" Watermark="dxc.example.net" />
<TextBox Name="HostBox" Grid.Row="1" PlaceholderText="dxc.example.net" />
<TextBlock Grid.Column="2" Text="Port" FontSize="11" Opacity="0.7" Margin="0,0,0,1" />
<TextBox Name="PortBox" Grid.Row="1" Grid.Column="2" />
</Grid>

View File

@@ -0,0 +1,35 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Nonemm.App.Dialogs.KeyerDialog"
Title="Keyer and messages" Width="560" Height="560"
WindowStartupLocation="CenterOwner">
<DockPanel Margin="14">
<StackPanel DockPanel.Dock="Top" Spacing="6">
<TextBlock TextWrapping="Wrap" FontSize="11" Opacity="0.75"
Text="cwdaemon keys the radio from a serial or parallel port and is what Linux stations usually run. A WinKeyer is on a serial port of its own." />
<Grid ColumnDefinitions="150,8,*,8,90" RowDefinitions="Auto,Auto">
<TextBlock Text="Keyer" FontSize="11" Opacity="0.7" Margin="0,6,0,1" />
<ComboBox Name="KindBox" Grid.Row="1" HorizontalAlignment="Stretch" />
<TextBlock Grid.Column="2" Name="TargetLabel" Text="cwdaemon host" FontSize="11" Opacity="0.7" Margin="0,6,0,1" />
<TextBox Name="TargetBox" Grid.Row="1" Grid.Column="2" />
<TextBlock Grid.Column="4" Text="Speed" FontSize="11" Opacity="0.7" Margin="0,6,0,1" />
<TextBox Name="SpeedBox" Grid.Row="1" Grid.Column="4" />
</Grid>
<TextBlock Text="Messages" FontSize="11" Opacity="0.7" Margin="0,10,0,1" />
<StackPanel Orientation="Horizontal" Spacing="6">
<RadioButton Name="CwButton" Content="CW" GroupName="mode" IsChecked="True" />
<RadioButton Name="PhoneButton" Content="Phone" GroupName="mode" />
</StackPanel>
</StackPanel>
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right"
Spacing="6" Margin="0,10,0,0">
<Button Content="Cancel" Click="OnCancel" />
<Button Content="Save" Click="OnSave" IsDefault="True" />
</StackPanel>
<ScrollViewer Margin="0,6,0,0">
<Grid Name="MessageGrid" ColumnDefinitions="60,*" />
</ScrollViewer>
</DockPanel>
</Window>

View File

@@ -0,0 +1,100 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Nonemm.App.Configuration;
namespace Nonemm.App.Dialogs;
/// Which keyer to use, and what the function keys send.
public sealed partial class KeyerDialog : Window
{
private readonly Settings settings;
private readonly List<TextBox> messageBoxes = [];
private List<string> cwMessages;
private List<string> phoneMessages;
public KeyerDialog(Settings settings)
{
this.settings = settings;
cwMessages = [.. Messages.For(Core.ModeCategory.Cw, settings.CwMessages, settings.PhoneMessages)];
phoneMessages = [.. Messages.For(Core.ModeCategory.Phone, settings.CwMessages, settings.PhoneMessages)];
InitializeComponent();
KindBox.ItemsSource = new[] { "none", "cwdaemon", "winkeyer" };
KindBox.SelectedItem = settings.KeyerKind;
KindBox.SelectionChanged += (_, _) => ShowTarget();
SpeedBox.Text = settings.KeyerSpeed.ToString();
CwButton.IsCheckedChanged += (_, _) => ShowMessages();
ShowTarget();
BuildMessageBoxes();
ShowMessages();
}
private void ShowTarget()
{
bool winkeyer = (KindBox.SelectedItem as string) == "winkeyer";
TargetLabel.Text = winkeyer ? "serial port" : "cwdaemon host:port";
TargetBox.Text = winkeyer
? settings.KeyerSerialPort
: $"{settings.KeyerHost}:{settings.KeyerPort}";
}
private void BuildMessageBoxes()
{
for (int at = 0; at < Messages.Keys.Count; at++)
{
MessageGrid.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
TextBlock label = new()
{
Text = Messages.Keys[at].Key,
FontSize = 12,
Margin = new Avalonia.Thickness(0, 6, 6, 0),
};
Grid.SetRow(label, at);
MessageGrid.Children.Add(label);
TextBox box = new() { Margin = new Avalonia.Thickness(0, 2, 0, 2) };
Grid.SetRow(box, at);
Grid.SetColumn(box, 1);
MessageGrid.Children.Add(box);
messageBoxes.Add(box);
}
}
private void ShowMessages()
{
List<string> shown = CwButton.IsChecked == true ? cwMessages : phoneMessages;
for (int at = 0; at < messageBoxes.Count; at++)
{
messageBoxes[at].Text = shown[at];
}
}
private void KeepMessages()
{
List<string> target = CwButton.IsChecked == true ? cwMessages : phoneMessages;
for (int at = 0; at < messageBoxes.Count; at++)
{
target[at] = messageBoxes[at].Text ?? "";
}
}
private void OnSave(object? sender, RoutedEventArgs e)
{
KeepMessages();
bool winkeyer = (KindBox.SelectedItem as string) == "winkeyer";
string[] target = (TargetBox.Text ?? "").Split(':');
Close(settings with
{
KeyerKind = KindBox.SelectedItem as string ?? "none",
KeyerSerialPort = winkeyer ? (TargetBox.Text ?? "").Trim() : settings.KeyerSerialPort,
KeyerHost = winkeyer ? settings.KeyerHost : target[0].Trim(),
KeyerPort = !winkeyer && target.Length > 1 && int.TryParse(target[1], out int port)
? port
: settings.KeyerPort,
KeyerSpeed = int.TryParse(SpeedBox.Text, out int speed) ? speed : settings.KeyerSpeed,
CwMessages = cwMessages,
PhoneMessages = phoneMessages,
});
}
private void OnCancel(object? sender, RoutedEventArgs e) => Close(null);
}

View File

@@ -13,7 +13,7 @@
<TextBox Name="PortBox" Grid.Row="1" Grid.Column="2" />
</Grid>
<TextBlock Text="Other stations, one address per line" FontSize="11" Opacity="0.7" Margin="0,6,0,1" />
<TextBox Name="PeersBox" AcceptsReturn="True" Height="90" Watermark="192.168.1.11" />
<TextBox Name="PeersBox" AcceptsReturn="True" Height="90" PlaceholderText="192.168.1.11" />
<CheckBox Name="EnabledBox" Content="Share contacts with the other stations" Margin="0,6,0,0" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="6" Margin="0,8,0,0">
<Button Content="Cancel" Click="OnCancel" />