Start the transmit pane again on every new message

The macro buttons went wrong at the end of a transmission, both faults in
DigitalEngineSender:

- The engine reports the transmitter drop on its own thread.
  WhenTransmitChanged took the state lock, decided the message had ended,
  released the lock, and only then called Buffer.Ended(), which clears
  what has gone to the engine. A macro pressed on the last character got
  through StartAsync in that gap and had already flushed its own text into
  Sent, so Ended() wiped the new text off the pane while the engine
  transmitted it. Ended() is now called inside the same lock.

- The pane only started again when the engine reported a drop. Two macros
  in a row keep the transmitter up, so that report never came and Sent
  grew with every press. Everything in Sent is locked, because it is in
  the engine and cannot be taken back, so the whole pane became
  read-only. TypeAhead.Started() drops the last message's sent text and
  keeps what was typed ahead, and StartAsync calls it whenever it keys a
  new transmission.

The rest of this commit is the digital transmit work these fixes sit on:
the pane as one coloured box, the sender's three keying states, the
type-ahead feeder paced by the clock with the engine's count as a brake,
{RX} flushing what is left in one piece, and the entry window's function
keys reading the digital macros on a digital mode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
This commit is contained in:
2026-09-03 14:40:30 +00:00
co-authored by Claude Opus 5
parent 7ae60be4a0
commit f49a8c10fd
20 changed files with 2604 additions and 390 deletions
+15 -29
View File
@@ -59,6 +59,15 @@ public sealed partial class DigitalWindow : RefreshableWindow
/// redraw is not read back as an edit by the operator.
private bool showingBuffer;
/// The transmit pane as it was last drawn, so an edit can be told from a
/// redraw and the changed character found.
private string pane = "";
/// How much of the pane is in the engine's hands and cannot be edited. It
/// is more than what is coloured, which is only what has gone out over the
/// air.
private int locked;
/// The buffer this window is drawing, or null while no engine is running.
private TypeAhead? buffer;
@@ -82,9 +91,6 @@ public sealed partial class DigitalWindow : RefreshableWindow
entry.Activated += WhenEntryActivated;
BuildMacros();
ApplySettings();
// the cursor holds the pump back, so it has to follow the caret as it
// moves, not only as the text changes
TransmitBox.GetObservable(TextBox.CaretIndexProperty).Subscribe(new Watcher(ShowCursor));
Attach(session.Digital);
Refresh();
Closed += (_, _) =>
@@ -104,8 +110,8 @@ public sealed partial class DigitalWindow : RefreshableWindow
{
ReceiveScroller.Background = Themes.Brush(Themes.Current.FieldBackground);
GrabScroller.Background = Themes.Brush(Themes.Current.FieldBackground);
SentText.Foreground = Transmitted;
SentText.FontSize = Settings.DigitalFontSize;
TransmitBox.Background = Themes.Brush(Themes.Current.FieldBackground);
TransmitBox.SentBrush = Transmitted;
TransmitBox.FontSize = Settings.DigitalFontSize;
if (buffer is not null)
{
@@ -188,21 +194,16 @@ public sealed partial class DigitalWindow : RefreshableWindow
private void WhenBufferChanged(object? sender, EventArgs e) =>
Dispatcher.UIThread.Post(ShowBuffer);
/// The pane starts empty for the next message: the transmitter dropping
/// with nothing left to send is the end of this one. An engine that keys
/// itself drops between two characters as well, and that text is left
/// where it is.
/// The transmitter dropping ends the message. The buffer clears what has
/// gone out and keeps what the operator typed ahead, so the pane is left
/// with the next message in it rather than empty.
private void WhenTransmitChanged(object? sender, bool transmitting) =>
Dispatcher.UIThread.Post(() =>
{
TransmitDot.Background = transmitting
? Themes.Brush(Themes.Current.TransmitLight)
: Brushes.Transparent;
if (!transmitting && buffer is { IsSending: false })
{
buffer.Clear();
ShowBuffer();
}
ShowBuffer();
});
private void WhenConnectionChanged(object? sender, bool connected) =>
@@ -568,18 +569,3 @@ public sealed partial class DigitalWindow : RefreshableWindow
_ => StackOrder.Disabled,
};
}
/// Watches one property. Avalonia hands out observables and this program has no
/// other use for Rx, so a handler that takes no value is enough.
internal sealed class Watcher(Action changed) : IObserver<int>
{
public void OnCompleted()
{
}
public void OnError(Exception error)
{
}
public void OnNext(int value) => changed();
}