Edit the function key messages as their file
The messages were twelve boxes in a dialog, which meant no labels, no search-and-pounce set, and no way to read a file somebody published. They are now the file itself: right-click any function key button under the entry window, or the two buttons in Config ▸ Keyer and messages, and the text of an N1MM .mc file opens in a plain editor with Import, Export and Back to the defaults. MessageFile reads that format by N1MM's rules. A line starting with # is a comment; every other line is a label, a comma and the message, with && standing for one & in a label. Which key a line belongs to is decided by where it is and nothing else: the first twelve lines are F1 to F12 while running, the next twelve the same keys while searching, and a file that stops part way through the second twelve leaves the rest sending what they send while running. A blank line in the middle is a key with nothing in it, which is what the manual says; the newline that ends the last line is not, which it does not say, but no .mc file in the world means its final newline as a message. Two things fall out of this. The buttons now say what the file says, and they change when the operator moves between running and searching, so the labels are the documentation N1MM's manual suggests writing them as. And the search set is real: ESM's F2 while searching can be a different message from the one it sends while running, which is how the file was always meant to be used. Settings keep the file text and carry the twelve stored messages into it on first read, with the labels this program used then, so nobody loses what they had typed. The editor is deliberately a text box rather than a grid of fields: importing, exporting and editing are then the same thing, and the comments an operator writes in the file survive being edited here. The telnet window's buttons can use the same editor later. Running it caught two: Save as the default button ate the Enter key that a multi-line editor needs, and the button labels did not follow the move between run and search until they were redrawn rather than only rebuilt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<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"
|
||||
Title="Keyer and messages" Width="560" SizeToContent="Height"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
<DockPanel Margin="14">
|
||||
<StackPanel DockPanel.Dock="Top" Spacing="6">
|
||||
@@ -23,9 +23,11 @@
|
||||
Text="Ctrl+B starts calling CQ on one radio and then the other. Needs two radios." />
|
||||
</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" />
|
||||
<TextBlock TextWrapping="Wrap" FontSize="11" Opacity="0.75"
|
||||
Text="The function keys are edited as their file, the same .mc file N1MM reads and writes. Right-clicking the buttons under the entry window opens the same editor." />
|
||||
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,6,0,0">
|
||||
<Button Content="Edit CW messages…" Click="OnEditCw" />
|
||||
<Button Content="Edit phone messages…" Click="OnEditPhone" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
@@ -35,8 +37,6 @@
|
||||
<Button Content="Save" Click="OnSave" IsDefault="True" />
|
||||
</StackPanel>
|
||||
|
||||
<ScrollViewer Margin="0,6,0,0">
|
||||
<Grid Name="MessageGrid" ColumnDefinitions="60,*" />
|
||||
</ScrollViewer>
|
||||
<Panel />
|
||||
</DockPanel>
|
||||
</Window>
|
||||
|
||||
@@ -1,32 +1,30 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Nonemm.App.Configuration;
|
||||
using Nonemm.Core;
|
||||
|
||||
namespace Nonemm.App.Dialogs;
|
||||
|
||||
/// Which keyer to use, and what the function keys send.
|
||||
/// Which keyer to use. The messages themselves are edited as their file, in
|
||||
/// `MessagesDialog`, which is what the buttons here open.
|
||||
public sealed partial class KeyerDialog : Window
|
||||
{
|
||||
private readonly Settings settings;
|
||||
private readonly List<TextBox> messageBoxes = [];
|
||||
private List<string> cwMessages;
|
||||
private List<string> phoneMessages;
|
||||
private string cwFile;
|
||||
private string phoneFile;
|
||||
|
||||
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)];
|
||||
cwFile = settings.CwMessageFile;
|
||||
phoneFile = settings.PhoneMessageFile;
|
||||
InitializeComponent();
|
||||
KindBox.ItemsSource = new[] { "none", "cwdaemon", "winkeyer" };
|
||||
KindBox.SelectedItem = settings.KeyerKind;
|
||||
KindBox.SelectionChanged += (_, _) => ShowTarget();
|
||||
SpeedBox.Text = settings.KeyerSpeed.ToString();
|
||||
AlternatingGapBox.Text = settings.AlternatingCqGapMs.ToString();
|
||||
CwButton.IsCheckedChanged += (_, _) => ShowMessages();
|
||||
ShowTarget();
|
||||
BuildMessageBoxes();
|
||||
ShowMessages();
|
||||
}
|
||||
|
||||
private void ShowTarget()
|
||||
@@ -38,49 +36,17 @@ public sealed partial class KeyerDialog : Window
|
||||
: $"{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);
|
||||
private async void OnEditCw(object? sender, RoutedEventArgs e) =>
|
||||
cwFile = await Edit(ModeCategory.Cw, cwFile);
|
||||
|
||||
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 async void OnEditPhone(object? sender, RoutedEventArgs e) =>
|
||||
phoneFile = await Edit(ModeCategory.Phone, phoneFile);
|
||||
|
||||
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 async Task<string> Edit(ModeCategory mode, string text) =>
|
||||
await new MessagesDialog(mode, text).ShowDialog<string?>(this) ?? text;
|
||||
|
||||
private void OnSave(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
KeepMessages();
|
||||
bool winkeyer = (KindBox.SelectedItem as string) == "winkeyer";
|
||||
string[] target = (TargetBox.Text ?? "").Split(':');
|
||||
Close(settings with
|
||||
@@ -96,8 +62,8 @@ public sealed partial class KeyerDialog : Window
|
||||
AlternatingCqGapMs = int.TryParse(AlternatingGapBox.Text, out int gap)
|
||||
? Math.Max(100, gap)
|
||||
: settings.AlternatingCqGapMs,
|
||||
CwMessages = cwMessages,
|
||||
PhoneMessages = phoneMessages,
|
||||
CwMessageFile = cwFile,
|
||||
PhoneMessageFile = phoneFile,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
24
src/Nonemm.App/Dialogs/MessagesDialog.axaml
Normal file
24
src/Nonemm.App/Dialogs/MessagesDialog.axaml
Normal file
@@ -0,0 +1,24 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Nonemm.App.Dialogs.MessagesDialog"
|
||||
Title="Function key messages" Width="620" Height="520"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
<DockPanel Margin="14">
|
||||
<TextBlock DockPanel.Dock="Top" TextWrapping="Wrap" FontSize="11" Opacity="0.75"
|
||||
Text="One line per key: the button label, a comma, then the message. A line starting with # is a comment. The first twelve lines are F1 to F12 while running; twelve more give the keys while searching, and the ones left out keep their running message. This is N1MM's .mc format, so a file from either program works in the other." />
|
||||
<Grid DockPanel.Dock="Bottom" ColumnDefinitions="Auto,*,Auto" Margin="0,10,0,0">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Button Content="Import…" Click="OnImport" />
|
||||
<Button Content="Export…" Click="OnExport" />
|
||||
<Button Content="Back to the defaults" Click="OnDefaults" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6">
|
||||
<Button Content="Cancel" Click="OnCancel" />
|
||||
<Button Content="Save" Click="OnSave" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<TextBox Name="TextArea" AcceptsReturn="True" AcceptsTab="False" FontFamily="monospace"
|
||||
FontSize="12" Margin="0,8,0,0" TextWrapping="NoWrap"
|
||||
VerticalContentAlignment="Top" />
|
||||
</DockPanel>
|
||||
</Window>
|
||||
82
src/Nonemm.App/Dialogs/MessagesDialog.axaml.cs
Normal file
82
src/Nonemm.App/Dialogs/MessagesDialog.axaml.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Nonemm.Core;
|
||||
|
||||
namespace Nonemm.App.Dialogs;
|
||||
|
||||
/// The function key messages as the text of an N1MM `.mc` file. Editing them as
|
||||
/// the file rather than as twelve boxes is what makes import and export the
|
||||
/// same thing as what is on the screen.
|
||||
///
|
||||
/// It closes with the text, or with null when nothing is to be changed.
|
||||
public sealed partial class MessagesDialog : Window
|
||||
{
|
||||
private static readonly FilePickerFileType MessageFiles = new("Function key files")
|
||||
{
|
||||
Patterns = ["*.mc"],
|
||||
};
|
||||
|
||||
private readonly ModeCategory mode;
|
||||
|
||||
public MessagesDialog(ModeCategory mode, string text, string what = "Function key messages")
|
||||
{
|
||||
this.mode = mode;
|
||||
InitializeComponent();
|
||||
Title = $"{what} — {(mode == ModeCategory.Phone ? "phone" : "CW")}";
|
||||
TextArea.Text = text.Trim().Length > 0 ? text : Messages.DefaultFor(mode);
|
||||
}
|
||||
|
||||
private async void OnImport(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
IReadOnlyList<IStorageFile> files = await StorageProvider.OpenFilePickerAsync(
|
||||
new FilePickerOpenOptions
|
||||
{
|
||||
Title = "Import a function key file",
|
||||
AllowMultiple = false,
|
||||
FileTypeFilter = [MessageFiles],
|
||||
});
|
||||
if (files.FirstOrDefault()?.TryGetLocalPath() is not { } path)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
TextArea.Text = await File.ReadAllTextAsync(path);
|
||||
}
|
||||
catch (Exception error) when (error is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
TextArea.Text = $"# {path} could not be read: {error.Message}\n{TextArea.Text}";
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnExport(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
IStorageFile? file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||
{
|
||||
Title = "Export the function key file",
|
||||
SuggestedFileName = mode == ModeCategory.Phone ? "phone.mc" : "cw.mc",
|
||||
DefaultExtension = "mc",
|
||||
FileTypeChoices = [MessageFiles],
|
||||
});
|
||||
if (file?.TryGetLocalPath() is not { } path)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(path, TextArea.Text ?? "");
|
||||
}
|
||||
catch (Exception error) when (error is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
TextArea.Text = $"# {path} could not be written: {error.Message}\n{TextArea.Text}";
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDefaults(object? sender, RoutedEventArgs e) =>
|
||||
TextArea.Text = Messages.DefaultFor(mode);
|
||||
|
||||
private void OnSave(object? sender, RoutedEventArgs e) => Close(TextArea.Text ?? "");
|
||||
|
||||
private void OnCancel(object? sender, RoutedEventArgs e) => Close(null);
|
||||
}
|
||||
Reference in New Issue
Block a user