Rework the telnet window's cluster favourites and login tabs

Favourites show the callsign in bold, the address and the sysop's note,
and each keeps its own password, commands, logon call and note. Clicking
one fills the boxes for editing. Autoconnect marks the favourite to
connect to at start and replaces the old reconnect-to-last-node setting.
A new published list refreshes the notes of the favourites in it. The
published list now spans the window, and the Spot comment tab becomes
Login & Spots with the logon call and keep-alive.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 13:03:13 +00:00
co-authored by Claude Sonnet 5.5
parent 7be51b72c7
commit d22787fb66
8 changed files with 286 additions and 99 deletions
+14 -7
View File
@@ -953,12 +953,18 @@ tabs, the ones N1MM has:
spot line, or "Jump to this spot" on the right-click menu, puts the radio spot line, or "Jump to this spot" on the right-click menu, puts the radio
there with the call in the entry window. Scrolling stops while the pointer is there with the call in the entry window. Scrolling stops while the pointer is
over the traffic, so a line can be read while the node keeps sending. over the traffic, so a line can be read while the node keeps sending.
- **Clusters** keeps a list of nodes with their names, ports, passwords and the - **Clusters** keeps a list of favourite nodes: the callsign in bold, the
commands to send after login. Connect and Disconnect are there, with automatic address and, under them, the sysop's note. Each favourite has its own password,
logon, the call to log on with, and the keep-alive interval. **Download** under its own commands to send after login and its own call to log on with (empty
it fetches the published list of nodes from means the one on the Login & Spots tab). Clicking a favourite fills the boxes
so it can be edited, and **Save** writes the change back. **Autoconnect** marks
one favourite as the node to connect to when the program starts and moves it to
the top; on that favourite the button reads **Don't connect**, and with no
favourite marked nothing connects at start. **Download**, across the bottom,
fetches the published list of nodes from
[NG3K](https://www.ng3k.com/misc/cluster.html) — around fifty nodes with the [NG3K](https://www.ng3k.com/misc/cluster.html) — around fifty nodes with the
sysop's call, the address and a note about each — and clicking one fills the sysop's call, the address and a note about each — and refreshes the notes of the
favourites that are in it. Clicking a node in the list fills the
boxes in. N1MM downloads its list from its own web service, which asks the boxes in. N1MM downloads its list from its own web service, which asks the
operator to opt in to data collection; this reads a public page instead. operator to opt in to data collection; this reads a public page instead.
- **Filters** says which spots reach the bandmap: bands, modes, beacons, busted - **Filters** says which spots reach the bandmap: bands, modes, beacons, busted
@@ -970,7 +976,8 @@ tabs, the ones N1MM has:
sends. sends.
- **Buttons** edits the twelve command buttons: a label and its commands, or - **Buttons** edits the twelve command buttons: a label and its commands, or
back to the defaults. back to the defaults.
- **Spot comment** is what goes out with the spots you send. - **Login & Spots** holds the call to log on with, the keep-alive interval and the
comment that goes out with the spots you send.
The buttons come from the settings; without any, they are `sh/dx`, `sh/dx/20`, The buttons come from the settings; without any, they are `sh/dx`, `sh/dx/20`,
`sh/wwv`, `sh/users`, `sh/c/n`, `help` and `bye`. A button holds what N1MM's `sh/wwv`, `sh/users`, `sh/c/n`, `help` and `bye`. A button holds what N1MM's
@@ -1020,7 +1027,7 @@ retried. The last two hundred lines are kept, so a window opened mid-contest is
not blank. not blank.
Alt+P, or **Edit → Spot It**, puts the call being typed on the cluster at the Alt+P, or **Edit → Spot It**, puts the call being typed on the cluster at the
current frequency, with the comment from the Spot comment tab — which takes the current frequency, with the comment from the Login & Spots tab — which takes the
message macros too; with nothing message macros too; with nothing
typed it spots the last contact logged. The spot also goes straight onto our own typed it spots the last contact logged. The spot also goes straight onto our own
bandmap rather than waiting to come back round from the node. bandmap rather than waiting to come back round from the node.
+25 -1
View File
@@ -941,6 +941,30 @@ public sealed partial class AppSession : IDisposable
radios.Clear(); radios.Clear();
} }
private static string FirstNonEmpty(params string[] values) =>
values.FirstOrDefault(v => v.Length > 0) ?? "";
/// The favourite the program connects to when it starts, if one is marked.
public StoredClusterNode? AutoConnectNode => Settings.ClusterNodes.FirstOrDefault(n => n.AutoConnect);
/// Connects to the favourite marked for it. Without one, nothing connects.
public void ConnectAutoCluster()
{
if (AutoConnectNode is not { } node)
{
return;
}
Save(Settings with
{
ClusterHost = node.Host,
ClusterPort = node.Port,
ClusterPassword = node.Password,
ClusterCommands = node.Commands,
ClusterNodeLogonCall = node.LogonCall,
});
ConnectCluster();
}
public void ConnectCluster() public void ConnectCluster()
{ {
cluster?.Dispose(); cluster?.Dispose();
@@ -948,7 +972,7 @@ public sealed partial class AppSession : IDisposable
cluster = new ClusterClient( cluster = new ClusterClient(
Settings.ClusterHost, Settings.ClusterHost,
Settings.ClusterPort, Settings.ClusterPort,
Settings.ClusterLogonCall.Length > 0 ? Settings.ClusterLogonCall : Settings.Station.Callsign, FirstNonEmpty(Settings.ClusterNodeLogonCall, Settings.ClusterLogonCall, Settings.Station.Callsign),
Settings.ClusterCommands, Settings.ClusterCommands,
Settings.ClusterPassword, Settings.ClusterPassword,
autoLogon: Settings.ClusterAutoLogon, autoLogon: Settings.ClusterAutoLogon,
+4 -3
View File
@@ -33,12 +33,15 @@ public sealed record Settings
/// tab. Connecting to one copies it into `ClusterHost` and the rest. /// tab. Connecting to one copies it into `ClusterHost` and the rest.
public IReadOnlyList<StoredClusterNode> ClusterNodes { get; init; } = []; public IReadOnlyList<StoredClusterNode> ClusterNodes { get; init; } = [];
/// Off for a node that wants the call typed in by hand. /// Always on now; the telnet window no longer has a switch for it.
public bool ClusterAutoLogon { get; init; } = true; public bool ClusterAutoLogon { get; init; } = true;
/// The call sent at login, or empty for the station callsign. /// The call sent at login, or empty for the station callsign.
public string ClusterLogonCall { get; init; } = ""; public string ClusterLogonCall { get; init; } = "";
/// The call the node being used wants instead of that one, or empty.
public string ClusterNodeLogonCall { get; init; } = "";
/// How often to send an empty line to a node that has said nothing. Nodes /// How often to send an empty line to a node that has said nothing. Nodes
/// drop a connection that has been quiet for a quarter of an hour. /// drop a connection that has been quiet for a quarter of an hour.
public int ClusterKeepAliveMinutes { get; init; } = 4; public int ClusterKeepAliveMinutes { get; init; } = 4;
@@ -147,8 +150,6 @@ public sealed record Settings
public bool RadioEnabled { get; init; } public bool RadioEnabled { get; init; }
public bool ClusterEnabled { get; init; }
public string NetworkStationName { get; init; } = ""; public string NetworkStationName { get; init; } = "";
public int NetworkPort { get; init; } = 12060; public int NetworkPort { get; init; } = 12060;
@@ -18,7 +18,26 @@ public sealed record StoredClusterNode
/// Sent to this node after login, one command per entry. /// Sent to this node after login, one command per entry.
public IReadOnlyList<string> Commands { get; init; } = []; public IReadOnlyList<string> Commands { get; init; } = [];
/// How the node reads in the favourites list. /// The call sent at login to this node, or empty for the one set on the
/// Login & Spots tab.
public string LogonCall { get; init; } = "";
/// What the published list says about the node. It is refreshed when a new
/// list is downloaded.
public string Description { get; init; } = "";
/// The node the program connects to when it starts. At most one favourite
/// has it.
public bool AutoConnect { get; init; }
[JsonIgnore] [JsonIgnore]
public string Label => Name.Length > 0 ? $"{Name} — {Host}:{Port}" : $"{Host}:{Port}"; public string Address => $"{Host}:{Port}";
/// The first line of the favourites list: the node's callsign, or its host
/// when it has none.
[JsonIgnore]
public string Title => Name.Length > 0 ? Name : Host;
[JsonIgnore]
public bool HasDescription => Description.Length > 0;
} }
+2 -2
View File
@@ -199,9 +199,9 @@ public sealed partial class EntryWindow : Window
{ {
yield return ("radios", session.ConnectRadios); yield return ("radios", session.ConnectRadios);
} }
if (session.Settings.ClusterEnabled) if (session.AutoConnectNode is not null)
{ {
yield return ("cluster", session.ConnectCluster); yield return ("cluster", session.ConnectAutoCluster);
} }
if (session.Settings.NetworkEnabled) if (session.Settings.NetworkEnabled)
{ {
+153 -31
View File
@@ -7,7 +7,8 @@ using Nonemm.Spotting;
namespace Nonemm.App.Windows; namespace Nonemm.App.Windows;
/// The Clusters tab: the nodes the operator keeps, the one being talked to, and /// The Clusters tab: the nodes the operator keeps, the one being talked to, and
/// how to log on to it. /// how to log on to it. The Login & Spots tab holds what is the same for every
/// node.
public sealed partial class TelnetWindow public sealed partial class TelnetWindow
{ {
private static readonly HttpClient Http = new() { Timeout = TimeSpan.FromSeconds(30) }; private static readonly HttpClient Http = new() { Timeout = TimeSpan.FromSeconds(30) };
@@ -15,22 +16,63 @@ public sealed partial class TelnetWindow
private readonly List<StoredClusterNode> nodes = []; private readonly List<StoredClusterNode> nodes = [];
private readonly List<ClusterNode> published = []; private readonly List<ClusterNode> published = [];
private void LoadClusters() /// What the published list says about the node in the boxes, kept for when
/// it is saved as a favourite.
private string pendingDescription = "";
/// True while the favourites list is being filled in, so the selection it
/// makes does not overwrite the boxes.
private bool refreshingNodes;
/// Fills the favourites, the boxes and the Login & Spots tab from the
/// settings. The list opens on `select`, or on the node the settings are
/// pointing at.
private void LoadClusters(StoredClusterNode? select = null)
{ {
nodes.Clear(); ShowNodes(select);
nodes.AddRange(session.Settings.ClusterNodes);
NodeList.ItemsSource = nodes.Select(n => n.Label).ToList();
NodeList.SelectedIndex = nodes.FindIndex(Matches);
Settings settings = session.Settings; Settings settings = session.Settings;
HostBox.Text = settings.ClusterHost; if (NodeList.SelectedIndex < 0)
PortBox.Text = settings.ClusterPort.ToString(CultureInfo.InvariantCulture); {
PasswordBox.Text = settings.ClusterPassword; HostBox.Text = settings.ClusterHost;
CommandsBox.Text = string.Join("\n", settings.ClusterCommands); PortBox.Text = settings.ClusterPort.ToString(CultureInfo.InvariantCulture);
PasswordBox.Text = settings.ClusterPassword;
NodeLogonBox.Text = settings.ClusterNodeLogonCall;
CommandsBox.Text = string.Join("\n", settings.ClusterCommands);
}
LoadPublishedNodes(); LoadPublishedNodes();
AutoLogonBox.IsChecked = settings.ClusterAutoLogon;
LogonCallBox.Text = settings.ClusterLogonCall; LogonCallBox.Text = settings.ClusterLogonCall;
KeepAliveBox.Text = settings.ClusterKeepAliveMinutes.ToString(CultureInfo.InvariantCulture); KeepAliveBox.Value = settings.ClusterKeepAliveMinutes;
SpotCommentBox.Text = settings.SpotComment;
UpdateNodeButtons();
}
/// Fills the list from `nodes` and puts the selection back.
private void ShowNodes(StoredClusterNode? select = null)
{
refreshingNodes = true;
nodes.Clear();
nodes.AddRange(session.Settings.ClusterNodes);
NodeList.ItemsSource = nodes.ToList();
NodeList.SelectedIndex = select is null
? nodes.FindIndex(Matches)
: nodes.FindIndex(n => SameNode(n, select));
refreshingNodes = false;
}
private static bool SameNode(StoredClusterNode a, StoredClusterNode b) =>
string.Equals(a.Host, b.Host, StringComparison.OrdinalIgnoreCase) && a.Port == b.Port;
/// The save button says "Save" for a favourite being edited, and the
/// autoconnect button follows the flag of the selected one.
private void UpdateNodeButtons()
{
StoredClusterNode? selected = NodeList.SelectedIndex >= 0 && NodeList.SelectedIndex < nodes.Count
? nodes[NodeList.SelectedIndex]
: null;
SaveNodeButton.Content = selected is null ? "Save to favourites" : "Save";
AutoConnectButton.IsEnabled = selected is not null;
AutoConnectButton.Content = selected is { AutoConnect: true } ? "Don't connect" : "Autoconnect";
} }
/// True for the node the settings are pointing at, so the list opens on it. /// True for the node the settings are pointing at, so the list opens on it.
@@ -38,22 +80,32 @@ public sealed partial class TelnetWindow
string.Equals(node.Host, session.Settings.ClusterHost, StringComparison.OrdinalIgnoreCase) string.Equals(node.Host, session.Settings.ClusterHost, StringComparison.OrdinalIgnoreCase)
&& node.Port == session.Settings.ClusterPort; && node.Port == session.Settings.ClusterPort;
/// A click on a favourite puts its fields in the boxes to be edited.
private void OnNodePicked(object? sender, SelectionChangedEventArgs e) private void OnNodePicked(object? sender, SelectionChangedEventArgs e)
{ {
if (NodeList.SelectedIndex < 0 || NodeList.SelectedIndex >= nodes.Count) if (refreshingNodes)
{ {
return; return;
} }
StoredClusterNode node = nodes[NodeList.SelectedIndex]; if (NodeList.SelectedIndex >= 0 && NodeList.SelectedIndex < nodes.Count)
NameBox.Text = node.Name; {
HostBox.Text = node.Host; StoredClusterNode node = nodes[NodeList.SelectedIndex];
PortBox.Text = node.Port.ToString(CultureInfo.InvariantCulture); PublishedNodeList.SelectedIndex = -1;
PasswordBox.Text = node.Password; NameBox.Text = node.Name;
CommandsBox.Text = string.Join("\n", node.Commands); HostBox.Text = node.Host;
PortBox.Text = node.Port.ToString(CultureInfo.InvariantCulture);
PasswordBox.Text = node.Password;
NodeLogonBox.Text = node.LogonCall;
CommandsBox.Text = string.Join("\n", node.Commands);
pendingDescription = node.Description;
}
UpdateNodeButtons();
} }
/// Adds what is in the boxes to the list, or replaces the entry that has /// With a favourite selected, saves the boxes over it, so its host can be
/// the same host and port. /// edited too. Otherwise adds a favourite, or replaces the one with the same
/// host and port. The auto-connect flag and the description stay with the
/// favourite.
private void OnStoreNode(object? sender, RoutedEventArgs e) private void OnStoreNode(object? sender, RoutedEventArgs e)
{ {
StoredClusterNode node = new() StoredClusterNode node = new()
@@ -62,16 +114,20 @@ public sealed partial class TelnetWindow
Host = Host(), Host = Host(),
Port = Port(), Port = Port(),
Password = PasswordBox.Text ?? "", Password = PasswordBox.Text ?? "",
LogonCall = (NodeLogonBox.Text ?? "").Trim(),
Commands = Lines(CommandsBox.Text), Commands = Lines(CommandsBox.Text),
Description = pendingDescription,
}; };
if (node.Host.Length == 0) if (node.Host.Length == 0)
{ {
return; return;
} }
int at = nodes.FindIndex(n => int at = NodeList.SelectedIndex >= 0 && NodeList.SelectedIndex < nodes.Count
string.Equals(n.Host, node.Host, StringComparison.OrdinalIgnoreCase) && n.Port == node.Port); ? NodeList.SelectedIndex
: nodes.FindIndex(n => SameNode(n, node));
if (at >= 0) if (at >= 0)
{ {
node = node with { AutoConnect = nodes[at].AutoConnect };
nodes[at] = node; nodes[at] = node;
} }
else else
@@ -79,7 +135,36 @@ public sealed partial class TelnetWindow
nodes.Add(node); nodes.Add(node);
} }
session.Save(session.Settings with { ClusterNodes = [.. nodes] }); session.Save(session.Settings with { ClusterNodes = [.. nodes] });
LoadClusters(); LoadClusters(node);
}
/// Marks the selected favourite as the one to connect to at start and moves
/// it to the top, or takes the mark off when it already has it.
private void OnToggleAutoConnect(object? sender, RoutedEventArgs e)
{
if (NodeList.SelectedIndex < 0 || NodeList.SelectedIndex >= nodes.Count)
{
return;
}
StoredClusterNode chosen = nodes[NodeList.SelectedIndex];
if (chosen.AutoConnect)
{
chosen = chosen with { AutoConnect = false };
nodes[NodeList.SelectedIndex] = chosen;
}
else
{
nodes.RemoveAt(NodeList.SelectedIndex);
chosen = chosen with { AutoConnect = true };
for (int i = 0; i < nodes.Count; i++)
{
nodes[i] = nodes[i] with { AutoConnect = false };
}
nodes.Insert(0, chosen);
}
session.Save(session.Settings with { ClusterNodes = [.. nodes] });
ShowNodes(chosen);
UpdateNodeButtons();
} }
private void OnRemoveNode(object? sender, RoutedEventArgs e) private void OnRemoveNode(object? sender, RoutedEventArgs e)
@@ -90,7 +175,9 @@ public sealed partial class TelnetWindow
} }
nodes.RemoveAt(NodeList.SelectedIndex); nodes.RemoveAt(NodeList.SelectedIndex);
session.Save(session.Settings with { ClusterNodes = [.. nodes] }); session.Save(session.Settings with { ClusterNodes = [.. nodes] });
LoadClusters(); ShowNodes();
NodeList.SelectedIndex = -1;
UpdateNodeButtons();
} }
/// The list downloaded from the published page, if one has been. Its /// The list downloaded from the published page, if one has been. Its
@@ -126,11 +213,15 @@ public sealed partial class TelnetWindow
return; return;
} }
ClusterNode node = published[PublishedNodeList.SelectedIndex]; ClusterNode node = published[PublishedNodeList.SelectedIndex];
NodeList.SelectedIndex = -1;
NameBox.Text = node.Name; NameBox.Text = node.Name;
HostBox.Text = node.Host; HostBox.Text = node.Host;
PortBox.Text = node.Port.ToString(CultureInfo.InvariantCulture); PortBox.Text = node.Port.ToString(CultureInfo.InvariantCulture);
PasswordBox.Text = ""; PasswordBox.Text = "";
NodeLogonBox.Text = "";
CommandsBox.Text = ""; CommandsBox.Text = "";
pendingDescription = node.Description;
UpdateNodeButtons();
} }
private async void OnDownloadNodes(object? sender, RoutedEventArgs e) private async void OnDownloadNodes(object? sender, RoutedEventArgs e)
@@ -147,6 +238,36 @@ public sealed partial class TelnetWindow
Show($"*** {error.Message}", NoticeColour); Show($"*** {error.Message}", NoticeColour);
} }
LoadPublishedNodes(); LoadPublishedNodes();
UpdateDescriptions();
}
/// Takes the descriptions of the list just downloaded into the favourites
/// that are in it, matched by host and port or by name.
private void UpdateDescriptions()
{
bool changed = false;
for (int i = 0; i < nodes.Count; i++)
{
ClusterNode? listed = published.FirstOrDefault(p =>
(string.Equals(p.Host, nodes[i].Host, StringComparison.OrdinalIgnoreCase) && p.Port == nodes[i].Port)
|| (nodes[i].Name.Length > 0 && string.Equals(p.Name, nodes[i].Name, StringComparison.OrdinalIgnoreCase)));
if (listed is not null && listed.Description != nodes[i].Description)
{
nodes[i] = nodes[i] with { Description = listed.Description };
changed = true;
}
}
if (!changed)
{
return;
}
StoredClusterNode? selected = NodeList.SelectedIndex >= 0 ? nodes[NodeList.SelectedIndex] : null;
session.Save(session.Settings with { ClusterNodes = [.. nodes] });
ShowNodes(selected);
if (selected is not null)
{
pendingDescription = nodes[NodeList.SelectedIndex].Description;
}
} }
/// `{CONN}name` connects to the favourite of that name rather than sending /// `{CONN}name` connects to the favourite of that name rather than sending
@@ -176,21 +297,22 @@ public sealed partial class TelnetWindow
private void OnDisconnect(object? sender, RoutedEventArgs e) private void OnDisconnect(object? sender, RoutedEventArgs e)
{ {
Forget(session.Cluster); Forget(session.Cluster);
session.Save(session.Settings with { ClusterEnabled = false });
session.DisconnectCluster(); session.DisconnectCluster();
Show("*** disconnected", NoticeColour); Show("*** disconnected", NoticeColour);
Refresh(); Refresh();
} }
private void OnSaveOptions(object? sender, RoutedEventArgs e) private void OnSaveLogin(object? sender, RoutedEventArgs e)
{ {
session.Save(session.Settings with session.Save(session.Settings with
{ {
ClusterAutoLogon = AutoLogonBox.IsChecked == true,
ClusterLogonCall = (LogonCallBox.Text ?? "").Trim(), ClusterLogonCall = (LogonCallBox.Text ?? "").Trim(),
ClusterKeepAliveMinutes = Minutes(KeepAliveBox.Text, session.Settings.ClusterKeepAliveMinutes), ClusterKeepAliveMinutes = KeepAliveBox.Value is { } minutes
? (int)minutes
: session.Settings.ClusterKeepAliveMinutes,
SpotComment = (SpotCommentBox.Text ?? "").Trim(),
}); });
Show("*** options saved — they apply on the next connection", NoticeColour); Show("*** login and spot comment saved — the login applies on the next connection", NoticeColour);
} }
/// Dials the node in the boxes and keeps it as the one to connect to on the /// Dials the node in the boxes and keeps it as the one to connect to on the
@@ -210,7 +332,7 @@ public sealed partial class TelnetWindow
ClusterPort = Port(), ClusterPort = Port(),
ClusterPassword = PasswordBox.Text ?? "", ClusterPassword = PasswordBox.Text ?? "",
ClusterCommands = Lines(CommandsBox.Text), ClusterCommands = Lines(CommandsBox.Text),
ClusterEnabled = true, ClusterNodeLogonCall = (NodeLogonBox.Text ?? "").Trim(),
}); });
Traffic.Items.Clear(); Traffic.Items.Clear();
session.ConnectCluster(); session.ConnectCluster();
@@ -5,8 +5,7 @@ using Nonemm.App.Configuration;
namespace Nonemm.App.Windows; namespace Nonemm.App.Windows;
/// The Filters and Spot comment tabs: which spots reach the bandmap, how long /// The Filters tab: which spots reach the bandmap.
/// they stay there, and what goes out with a spot the operator sends.
public sealed partial class TelnetWindow public sealed partial class TelnetWindow
{ {
/// The bands worth a tick box. The rest of the microwave bands would fill /// The bands worth a tick box. The rest of the microwave bands would fill
@@ -48,7 +47,6 @@ public sealed partial class TelnetWindow
CallAreasBox.Text = string.Join(' ', filter.CallAreas); CallAreasBox.Text = string.Join(' ', filter.CallAreas);
BlockedSpottersBox.Text = string.Join(' ', filter.BlockedSpotters); BlockedSpottersBox.Text = string.Join(' ', filter.BlockedSpotters);
BlockedCallsBox.Text = string.Join(' ', filter.BlockedCalls); BlockedCallsBox.Text = string.Join(' ', filter.BlockedCalls);
SpotCommentBox.Text = session.Settings.SpotComment;
} }
private void OnApplyFilters(object? sender, RoutedEventArgs e) private void OnApplyFilters(object? sender, RoutedEventArgs e)
@@ -74,12 +72,6 @@ public sealed partial class TelnetWindow
Show("*** filters applied — they hold for the spots that arrive from now on", NoticeColour); Show("*** filters applied — they hold for the spots that arrive from now on", NoticeColour);
} }
private void OnSaveSpotComment(object? sender, RoutedEventArgs e)
{
session.Save(session.Settings with { SpotComment = (SpotCommentBox.Text ?? "").Trim() });
Show("*** spot comment saved", NoticeColour);
}
private IReadOnlyList<string> Modes() private IReadOnlyList<string> Modes()
{ {
List<string> modes = []; List<string> modes = [];
+66 -44
View File
@@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Nonemm.App.Windows" xmlns:local="using:Nonemm.App.Windows"
xmlns:theming="using:Nonemm.App.Theming" xmlns:theming="using:Nonemm.App.Theming"
xmlns:config="using:Nonemm.App.Configuration"
x:Class="Nonemm.App.Windows.TelnetWindow" x:Class="Nonemm.App.Windows.TelnetWindow"
Title="Telnet" Width="760" Height="520"> Title="Telnet" Width="760" Height="520">
<Window.Styles> <Window.Styles>
@@ -47,50 +48,65 @@
</TabItem> </TabItem>
<TabItem Header="Clusters"> <TabItem Header="Clusters">
<Grid ColumnDefinitions="*,12,300" Margin="10"> <Grid RowDefinitions="3*,12,2*" Margin="10">
<Grid RowDefinitions="Auto,2*,Auto,Auto,3*"> <Grid ColumnDefinitions="*,12,300">
<TextBlock Classes="label" Text="Favourites" Margin="0,0,0,1" /> <Grid RowDefinitions="Auto,*,Auto">
<ListBox Grid.Row="1" Name="NodeList" SelectionChanged="OnNodePicked" /> <TextBlock Classes="label" Text="Favourites" Margin="0,0,0,1" />
<StackPanel Grid.Row="2" Orientation="Horizontal" Spacing="6" Margin="0,6,0,0"> <ListBox Grid.Row="1" Name="NodeList" SelectionChanged="OnNodePicked">
<Button Content="Connect" Click="OnConnect" /> <ListBox.ItemTemplate>
<Button Content="Disconnect" Click="OnDisconnect" /> <DataTemplate x:DataType="config:StoredClusterNode">
<Button Content="Remove" Click="OnRemoveNode" /> <StackPanel>
</StackPanel> <StackPanel Orientation="Horizontal" Spacing="8">
<Grid Grid.Row="3" ColumnDefinitions="*,Auto" Margin="0,12,0,1"> <TextBlock Text="{Binding Title}" FontWeight="Bold" />
<TextBlock Text="{Binding Address}" Opacity="0.75" />
<TextBlock Text="auto-connect" FontSize="11" VerticalAlignment="Center"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}"
IsVisible="{Binding AutoConnect}" />
</StackPanel>
<TextBlock Text="{Binding Description}" FontSize="11" Opacity="0.7"
IsVisible="{Binding HasDescription}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Grid.Row="2" Orientation="Horizontal" Spacing="6" Margin="0,6,0,0">
<Button Content="Connect" Click="OnConnect" />
<Button Content="Disconnect" Click="OnDisconnect" />
<Button Name="AutoConnectButton" Content="Autoconnect" Click="OnToggleAutoConnect" />
<Button Content="Remove" Click="OnRemoveNode" />
</StackPanel>
</Grid>
<ScrollViewer Grid.Column="2">
<StackPanel>
<TextBlock Classes="label" Text="Name" Margin="0,0,0,1" />
<TextBox Name="NameBox" />
<Grid ColumnDefinitions="*,8,80">
<StackPanel>
<TextBlock Classes="label" Text="Node" />
<TextBox Name="HostBox" PlaceholderText="dxc.example.net" />
</StackPanel>
<StackPanel Grid.Column="2">
<TextBlock Classes="label" Text="Port" />
<TextBox Name="PortBox" />
</StackPanel>
</Grid>
<TextBlock Classes="label" Text="Password, if the node asks for one" />
<TextBox Name="PasswordBox" PasswordChar="•" />
<TextBlock Classes="label" Text="Log on with" />
<TextBox Name="NodeLogonBox" PlaceholderText="Same as in Login &amp; Spots" />
<TextBlock Classes="label" Text="Commands sent after login, one per line" />
<TextBox Name="CommandsBox" AcceptsReturn="True" Height="70" />
<Button Name="SaveNodeButton" Content="Save to favourites" Click="OnStoreNode" Margin="0,8,0,0" />
</StackPanel>
</ScrollViewer>
</Grid>
<Grid Grid.Row="2" RowDefinitions="Auto,*">
<Grid ColumnDefinitions="*,Auto" Margin="0,0,0,1">
<TextBlock Classes="label" Name="PublishedText" Text="Published list" VerticalAlignment="Center" /> <TextBlock Classes="label" Name="PublishedText" Text="Published list" VerticalAlignment="Center" />
<Button Grid.Column="1" Content="Download" Click="OnDownloadNodes" /> <Button Grid.Column="1" Content="Download" Click="OnDownloadNodes" />
</Grid> </Grid>
<ListBox Grid.Row="4" Name="PublishedNodeList" SelectionChanged="OnPublishedNodePicked" /> <ListBox Grid.Row="1" Name="PublishedNodeList" SelectionChanged="OnPublishedNodePicked" />
</Grid> </Grid>
<ScrollViewer Grid.Column="2">
<StackPanel>
<TextBlock Classes="label" Text="Name" Margin="0,0,0,1" />
<TextBox Name="NameBox" />
<Grid ColumnDefinitions="*,8,80" >
<StackPanel>
<TextBlock Classes="label" Text="Node" />
<TextBox Name="HostBox" PlaceholderText="dxc.example.net" />
</StackPanel>
<StackPanel Grid.Column="2">
<TextBlock Classes="label" Text="Port" />
<TextBox Name="PortBox" />
</StackPanel>
</Grid>
<TextBlock Classes="label" Text="Password, if the node asks for one" />
<TextBox Name="PasswordBox" PasswordChar="•" />
<TextBlock Classes="label" Text="Commands sent after login, one per line" />
<TextBox Name="CommandsBox" AcceptsReturn="True" Height="70" />
<Button Content="Save to favourites" Click="OnStoreNode" Margin="0,8,0,0" />
<TextBlock Classes="label" Text="Options" Margin="0,14,0,1" />
<CheckBox Name="AutoLogonBox" Content="Log on automatically" />
<TextBlock Classes="label" Text="Log on with" />
<TextBox Name="LogonCallBox" PlaceholderText="the station callsign" />
<TextBlock Classes="label" Text="Keep alive every (minutes)" />
<TextBox Name="KeepAliveBox" Width="60" HorizontalAlignment="Left" />
<Button Content="Save options" Click="OnSaveOptions" Margin="0,8,0,0" />
</StackPanel>
</ScrollViewer>
</Grid> </Grid>
</TabItem> </TabItem>
@@ -139,11 +155,17 @@
</DockPanel> </DockPanel>
</TabItem> </TabItem>
<TabItem Header="Spot comment"> <TabItem Header="Login &amp; Spots">
<StackPanel Margin="10"> <StackPanel Margin="10" Width="300" HorizontalAlignment="Left">
<TextBlock Classes="label" Text="Sent with the spots you put on the cluster" /> <TextBlock Classes="label" Text="Log on with" Margin="0,0,0,1" />
<TextBox Name="LogonCallBox" PlaceholderText="the station callsign" />
<TextBlock Classes="label" Text="Keep alive every (minutes)" />
<NumericUpDown Name="KeepAliveBox" Minimum="1" Maximum="60" Increment="1"
FormatString="0" HorizontalAlignment="Left" Width="120" />
<TextBlock Classes="label" Text="Spot comment, sent with the spots you put on the cluster" />
<TextBox Name="SpotCommentBox" PlaceholderText="CQ contest" /> <TextBox Name="SpotCommentBox" PlaceholderText="CQ contest" />
<Button Content="Save" Click="OnSaveSpotComment" Margin="0,10,0,0" /> <Button Content="Save" Click="OnSaveLogin" Margin="0,10,0,0" />
<TextBlock Classes="label" Text="The login applies on the next connection." />
</StackPanel> </StackPanel>
</TabItem> </TabItem>
</TabControl> </TabControl>