Show what has gone out in the transmit pane
The transmit pane is now the type-ahead buffer. What has gone to the engine is in red beside the box and cannot be reached; what is still to go is in the box and can be typed over, added to or deleted while the engine works through it. A macro fills the pane rather than the engine, so the operator can correct a call the message is still sending. The cursor holds the pump: nothing behind it goes out, so an engine that catches up with the operator idles instead of transmitting half a word. With the box out of focus there is no cursor and everything goes. Escape and the RX button drop what has not gone out and stop the engine where it is. Enter puts in the carriage return the engine takes as a new line. The pane starts empty for the next message. The baud rate the pump counts by is in the digital setup window, beside the alignment frequency. It has to match the engine: 45.45 unless the contest says otherwise. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
This commit is contained in:
@@ -132,7 +132,7 @@ public sealed class AppSession : IDisposable
|
|||||||
public MmttyEngine? Digital => digital;
|
public MmttyEngine? Digital => digital;
|
||||||
|
|
||||||
/// The same engine as something a macro can be sent through.
|
/// The same engine as something a macro can be sent through.
|
||||||
public MessageSender? DigitalKeyer => digitalSender;
|
public DigitalEngineSender? DigitalKeyer => digitalSender;
|
||||||
|
|
||||||
/// Alternating CQ, or null while there is no keyer. It needs two radios to
|
/// Alternating CQ, or null while there is no keyer. It needs two radios to
|
||||||
/// do anything, and a keyer that reports when a message has gone out.
|
/// do anything, and a keyer that reports when a message has gone out.
|
||||||
@@ -333,7 +333,7 @@ public sealed class AppSession : IDisposable
|
|||||||
MmttyEngine started = new(channel, options);
|
MmttyEngine started = new(channel, options);
|
||||||
await started.StartAsync();
|
await started.StartAsync();
|
||||||
digital = started;
|
digital = started;
|
||||||
digitalSender = new DigitalEngineSender(started);
|
digitalSender = new DigitalEngineSender(started, Settings.DigitalBaud);
|
||||||
Changed?.Invoke(this, EventArgs.Empty);
|
Changed?.Invoke(this, EventArgs.Empty);
|
||||||
return started;
|
return started;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,6 +255,11 @@ public sealed record Settings
|
|||||||
/// fills the report in, the way typing one does.
|
/// fills the report in, the way typing one does.
|
||||||
public bool DigitalGrabSendsSpace { get; init; } = true;
|
public bool DigitalGrabSendsSpace { get; init; } = true;
|
||||||
|
|
||||||
|
/// The speed the engine transmits at. The type-ahead buffer is paced by
|
||||||
|
/// it, so a station running 75 baud sets it here as well as in the engine.
|
||||||
|
/// MMTTY's own default, and what nearly every RTTY contest runs at.
|
||||||
|
public double DigitalBaud { get; init; } = 45.45;
|
||||||
|
|
||||||
/// N1MM caps the receive pane's font at 14 points.
|
/// N1MM caps the receive pane's font at 14 points.
|
||||||
public int DigitalFontSize { get; init; } = 12;
|
public int DigitalFontSize { get; init; } = 12;
|
||||||
|
|
||||||
|
|||||||
@@ -103,13 +103,17 @@
|
|||||||
</Border>
|
</Border>
|
||||||
</HeaderedContentControl>
|
</HeaderedContentControl>
|
||||||
|
|
||||||
<HeaderedContentControl Header="Alignment Frequency">
|
<HeaderedContentControl Header="Alignment Frequency and Speed">
|
||||||
<Border BorderThickness="1" BorderBrush="#40808080" Padding="8">
|
<Border BorderThickness="1" BorderBrush="#40808080" Padding="8">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Classes="label" Text="MMTTY mark frequency (Hz)" Margin="0,0,0,1" />
|
<TextBlock Classes="label" Text="MMTTY mark frequency (Hz)" Margin="0,0,0,1" />
|
||||||
<TextBox Name="MarkBox" Width="120" HorizontalAlignment="Left" />
|
<TextBox Name="MarkBox" Width="120" HorizontalAlignment="Left" />
|
||||||
<TextBlock Classes="label" TextWrapping="Wrap"
|
<TextBlock Classes="label" TextWrapping="Wrap"
|
||||||
Text="The Align button retunes the radio so the signal that was clicked in the waterfall sits on this tone." />
|
Text="The Align button retunes the radio so the signal that was clicked in the waterfall sits on this tone." />
|
||||||
|
<TextBlock Classes="label" Text="Baud rate" Margin="0,8,0,1" />
|
||||||
|
<TextBox Name="BaudBox" Width="120" HorizontalAlignment="Left" />
|
||||||
|
<TextBlock Classes="label" TextWrapping="Wrap"
|
||||||
|
Text="The speed the engine transmits at, which paces the transmit pane. Set it to the same speed as the engine: 45.45 baud unless the contest says otherwise." />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</HeaderedContentControl>
|
</HeaderedContentControl>
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ public sealed partial class DigitalDialog : Window
|
|||||||
ForegroundHighlightBox.IsChecked = !settings.DigitalHighlightBackground;
|
ForegroundHighlightBox.IsChecked = !settings.DigitalHighlightBackground;
|
||||||
BackgroundHighlightBox.IsChecked = settings.DigitalHighlightBackground;
|
BackgroundHighlightBox.IsChecked = settings.DigitalHighlightBackground;
|
||||||
MarkBox.Text = settings.DigitalMarkHertz.ToString(CultureInfo.InvariantCulture);
|
MarkBox.Text = settings.DigitalMarkHertz.ToString(CultureInfo.InvariantCulture);
|
||||||
|
BaudBox.Text = settings.DigitalBaud.ToString(CultureInfo.InvariantCulture);
|
||||||
WindowBox.ItemsSource = WindowSizes;
|
WindowBox.ItemsSource = WindowSizes;
|
||||||
WindowBox.SelectedItem =
|
WindowBox.SelectedItem =
|
||||||
WindowSizes.FirstOrDefault(s => s == settings.DigitalEngineWindow) ?? WindowSizes[0];
|
WindowSizes.FirstOrDefault(s => s == settings.DigitalEngineWindow) ?? WindowSizes[0];
|
||||||
@@ -300,6 +301,10 @@ public sealed partial class DigitalDialog : Window
|
|||||||
DigitalWineCommand = WineBox.Text?.Trim() is { Length: > 0 } wine ? wine : "wine",
|
DigitalWineCommand = WineBox.Text?.Trim() is { Length: > 0 } wine ? wine : "wine",
|
||||||
DigitalPttPort = PttBox.Text?.Trim() ?? "",
|
DigitalPttPort = PttBox.Text?.Trim() ?? "",
|
||||||
DigitalMarkHertz = int.TryParse(MarkBox.Text, out int mark) ? mark : settings.DigitalMarkHertz,
|
DigitalMarkHertz = int.TryParse(MarkBox.Text, out int mark) ? mark : settings.DigitalMarkHertz,
|
||||||
|
DigitalBaud = double.TryParse(BaudBox.Text, NumberStyles.Float, CultureInfo.InvariantCulture,
|
||||||
|
out double baud) && baud > 0
|
||||||
|
? baud
|
||||||
|
: settings.DigitalBaud,
|
||||||
DigitalEngineWindow = WindowBox.SelectedItem as string ?? "Normal",
|
DigitalEngineWindow = WindowBox.SelectedItem as string ?? "Normal",
|
||||||
DigitalEngineOnTop = OnTopBox.IsChecked == true,
|
DigitalEngineOnTop = OnTopBox.IsChecked == true,
|
||||||
DigitalEnabled = (EngineBox.Text?.Trim().Length ?? 0) > 0,
|
DigitalEnabled = (EngineBox.Text?.Trim().Length ?? 0) > 0,
|
||||||
|
|||||||
@@ -119,9 +119,7 @@ public sealed partial class DigitalWindow
|
|||||||
}
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string text = await File.ReadAllTextAsync(picked[0].Path.LocalPath);
|
await keyer.SendAsync(await File.ReadAllTextAsync(picked[0].Path.LocalPath));
|
||||||
TransmitBox.Text += text;
|
|
||||||
await keyer.SendAsync(text);
|
|
||||||
}
|
}
|
||||||
catch (Exception failure) when (failure is IOException or InvalidOperationException)
|
catch (Exception failure) when (failure is IOException or InvalidOperationException)
|
||||||
{
|
{
|
||||||
@@ -129,27 +127,85 @@ public sealed partial class DigitalWindow
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What is typed in the transmit pane goes out as it is typed, which is how
|
/// What is typed in the transmit pane goes into the type-ahead buffer,
|
||||||
/// N1MM's window works: the engine transmits each character as it arrives.
|
/// which feeds the engine. A carriage return is what the engine takes as a
|
||||||
|
/// new line, so Enter puts one in rather than the newline the box would.
|
||||||
private void OnTransmitKeyDown(object? sender, KeyEventArgs e)
|
private void OnTransmitKeyDown(object? sender, KeyEventArgs e)
|
||||||
{
|
{
|
||||||
if (engine is not { IsConnected: true } running)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (e.Key == Key.Enter)
|
if (e.Key == Key.Enter)
|
||||||
{
|
{
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
_ = running.TypeAsync('\r');
|
int at = Math.Clamp(TransmitBox.CaretIndex, 0, TransmitBox.Text?.Length ?? 0);
|
||||||
TransmitBox.Text += "\n";
|
TransmitBox.Text = (TransmitBox.Text ?? "").Insert(at, "\r");
|
||||||
TransmitBox.CaretIndex = TransmitBox.Text.Length;
|
TransmitBox.CaretIndex = at + 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.Key == Key.Escape)
|
if (e.Key == Key.Escape)
|
||||||
{
|
{
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
_ = running.AbortAsync();
|
_ = session.DigitalKeyer?.AbortAsync();
|
||||||
TransmitBox.Text = "";
|
_ = Running()?.AbortAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The operator rewrote what has not gone out yet. Only the box holds it;
|
||||||
|
/// what is already in the engine is in the label beside it and cannot be
|
||||||
|
/// reached from here.
|
||||||
|
private void OnTransmitTextChanged(object? sender, TextChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (showingBuffer || session.DigitalKeyer is not { } keyer)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
keyer.TypeAhead.Rewrite(TransmitBox.Text ?? "", CursorInBox());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The cursor holds the pump back: nothing behind it goes out, so the
|
||||||
|
/// engine idles rather than transmitting text the operator is still
|
||||||
|
/// typing. With the box out of focus there is no cursor to hold anything.
|
||||||
|
private void OnTransmitFocus(object? sender, RoutedEventArgs e) => ShowCursor();
|
||||||
|
|
||||||
|
private void ShowCursor()
|
||||||
|
{
|
||||||
|
if (session.DigitalKeyer is { } keyer)
|
||||||
|
{
|
||||||
|
keyer.TypeAhead.Cursor = TransmitBox.IsFocused ? CursorInBox() : TypeAhead.NoCursor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int CursorInBox() =>
|
||||||
|
Math.Clamp(TransmitBox.CaretIndex, 0, TransmitBox.Text?.Length ?? 0);
|
||||||
|
|
||||||
|
/// Draws the buffer: what has gone out in the label, what is still to go in
|
||||||
|
/// the box. The pump takes characters off the front, so the cursor moves
|
||||||
|
/// back with them and the operator can go on typing while it does.
|
||||||
|
private void ShowBuffer()
|
||||||
|
{
|
||||||
|
if (session.DigitalKeyer is not { } keyer)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showingBuffer = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SentText.Text = keyer.TypeAhead.Sent;
|
||||||
|
string pending = keyer.TypeAhead.Pending;
|
||||||
|
string was = TransmitBox.Text ?? "";
|
||||||
|
if (was == pending)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int taken = was.Length > pending.Length
|
||||||
|
&& was.EndsWith(pending, StringComparison.Ordinal)
|
||||||
|
? was.Length - pending.Length
|
||||||
|
: 0;
|
||||||
|
int caret = TransmitBox.CaretIndex;
|
||||||
|
TransmitBox.Text = pending;
|
||||||
|
TransmitBox.CaretIndex = Math.Clamp(caret - taken, 0, pending.Length);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
showingBuffer = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,6 +251,7 @@ public sealed partial class DigitalWindow
|
|||||||
}
|
}
|
||||||
if (running.IsTransmitting)
|
if (running.IsTransmitting)
|
||||||
{
|
{
|
||||||
|
_ = session.DigitalKeyer?.AbortAsync();
|
||||||
_ = running.ReturnToReceiveAsync();
|
_ = running.ReturnToReceiveAsync();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -208,9 +265,19 @@ public sealed partial class DigitalWindow
|
|||||||
TransmitBox.Focus();
|
TransmitBox.Focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnReceive(object? sender, RoutedEventArgs e) => _ = Running()?.AbortAsync();
|
/// The RX button stops where it is: what has not gone to the engine is
|
||||||
|
/// dropped, and the engine drops what it holds.
|
||||||
|
private void OnReceive(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_ = session.DigitalKeyer?.AbortAsync();
|
||||||
|
_ = Running()?.AbortAsync();
|
||||||
|
}
|
||||||
|
|
||||||
private void OnClearTransmit(object? sender, RoutedEventArgs e) => TransmitBox.Text = "";
|
private void OnClearTransmit(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
session.DigitalKeyer?.TypeAhead.Clear();
|
||||||
|
ShowBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
private void OnClearReceive(object? sender, RoutedEventArgs e)
|
private void OnClearReceive(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -166,9 +166,17 @@
|
|||||||
|
|
||||||
<Grid Grid.Row="2" ColumnDefinitions="*,Auto,150">
|
<Grid Grid.Row="2" ColumnDefinitions="*,Auto,150">
|
||||||
<Border BorderThickness="1" BorderBrush="#40808080">
|
<Border BorderThickness="1" BorderBrush="#40808080">
|
||||||
<TextBox Name="TransmitBox" AcceptsReturn="True" TextWrapping="Wrap"
|
<!-- what has gone out, then what is still to go: the first is a
|
||||||
FontFamily="monospace" BorderThickness="0"
|
label so it cannot be edited, the second the box the operator
|
||||||
KeyDown="OnTransmitKeyDown" />
|
types in -->
|
||||||
|
<Grid ColumnDefinitions="Auto,*">
|
||||||
|
<TextBlock Name="SentText" FontFamily="monospace" Margin="3,3,0,0"
|
||||||
|
VerticalAlignment="Top" />
|
||||||
|
<TextBox Grid.Column="1" Name="TransmitBox" AcceptsReturn="True" TextWrapping="Wrap"
|
||||||
|
FontFamily="monospace" BorderThickness="0"
|
||||||
|
KeyDown="OnTransmitKeyDown" TextChanged="OnTransmitTextChanged"
|
||||||
|
GotFocus="OnTransmitFocus" LostFocus="OnTransmitFocus" />
|
||||||
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
<Grid Grid.Column="2" RowDefinitions="*,Auto">
|
<Grid Grid.Column="2" RowDefinitions="*,Auto">
|
||||||
<Border BorderThickness="1" BorderBrush="#40808080">
|
<Border BorderThickness="1" BorderBrush="#40808080">
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ public sealed partial class DigitalWindow : RefreshableWindow
|
|||||||
/// What N1MM marks a call its resource files do not hold with.
|
/// What N1MM marks a call its resource files do not hold with.
|
||||||
private static readonly IBrush Unknown = Brushes.Gold;
|
private static readonly IBrush Unknown = Brushes.Gold;
|
||||||
|
|
||||||
|
/// What is drawn over the text that has already gone to the engine.
|
||||||
|
private static readonly IBrush Transmitted = new SolidColorBrush(Color.FromRgb(0xC0, 0x39, 0x2B));
|
||||||
|
|
||||||
private readonly AppSession session;
|
private readonly AppSession session;
|
||||||
private readonly EntryWindow entry;
|
private readonly EntryWindow entry;
|
||||||
private readonly int radioNumber;
|
private readonly int radioNumber;
|
||||||
@@ -52,6 +55,13 @@ public sealed partial class DigitalWindow : RefreshableWindow
|
|||||||
private bool paused;
|
private bool paused;
|
||||||
private bool lastWasReturn;
|
private bool lastWasReturn;
|
||||||
|
|
||||||
|
/// True while the transmit box is being redrawn from the buffer, so the
|
||||||
|
/// redraw is not read back as an edit by the operator.
|
||||||
|
private bool showingBuffer;
|
||||||
|
|
||||||
|
/// The buffer this window is drawing, or null while no engine is running.
|
||||||
|
private TypeAhead? buffer;
|
||||||
|
|
||||||
/// Where the next line goes in a non-scrolling pane. It walks down the
|
/// Where the next line goes in a non-scrolling pane. It walks down the
|
||||||
/// window and starts again at the top; -1 is before the first line.
|
/// window and starts again at the top; -1 is before the first line.
|
||||||
private int lineAt = -1;
|
private int lineAt = -1;
|
||||||
@@ -72,6 +82,9 @@ public sealed partial class DigitalWindow : RefreshableWindow
|
|||||||
entry.Activated += WhenEntryActivated;
|
entry.Activated += WhenEntryActivated;
|
||||||
BuildMacros();
|
BuildMacros();
|
||||||
ApplySettings();
|
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);
|
Attach(session.Digital);
|
||||||
Refresh();
|
Refresh();
|
||||||
Closed += (_, _) =>
|
Closed += (_, _) =>
|
||||||
@@ -91,6 +104,13 @@ public sealed partial class DigitalWindow : RefreshableWindow
|
|||||||
{
|
{
|
||||||
ReceiveScroller.Background = Themes.Brush(Themes.Current.FieldBackground);
|
ReceiveScroller.Background = Themes.Brush(Themes.Current.FieldBackground);
|
||||||
GrabScroller.Background = Themes.Brush(Themes.Current.FieldBackground);
|
GrabScroller.Background = Themes.Brush(Themes.Current.FieldBackground);
|
||||||
|
SentText.Foreground = Transmitted;
|
||||||
|
SentText.FontSize = Settings.DigitalFontSize;
|
||||||
|
TransmitBox.FontSize = Settings.DigitalFontSize;
|
||||||
|
if (buffer is not null)
|
||||||
|
{
|
||||||
|
buffer.Baud = Settings.DigitalBaud;
|
||||||
|
}
|
||||||
FollowRunAndFrequency();
|
FollowRunAndFrequency();
|
||||||
ShowState();
|
ShowState();
|
||||||
ShowGrabList();
|
ShowGrabList();
|
||||||
@@ -127,6 +147,12 @@ public sealed partial class DigitalWindow : RefreshableWindow
|
|||||||
}
|
}
|
||||||
Detach();
|
Detach();
|
||||||
engine = started;
|
engine = started;
|
||||||
|
buffer = session.DigitalKeyer?.TypeAhead;
|
||||||
|
if (buffer is not null)
|
||||||
|
{
|
||||||
|
buffer.Baud = Settings.DigitalBaud;
|
||||||
|
buffer.Changed += WhenBufferChanged;
|
||||||
|
}
|
||||||
started.Received += WhenReceived;
|
started.Received += WhenReceived;
|
||||||
started.TransmitChanged += WhenTransmitChanged;
|
started.TransmitChanged += WhenTransmitChanged;
|
||||||
started.ConnectionChanged += WhenConnectionChanged;
|
started.ConnectionChanged += WhenConnectionChanged;
|
||||||
@@ -139,6 +165,11 @@ public sealed partial class DigitalWindow : RefreshableWindow
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (buffer is not null)
|
||||||
|
{
|
||||||
|
buffer.Changed -= WhenBufferChanged;
|
||||||
|
buffer = null;
|
||||||
|
}
|
||||||
engine.Received -= WhenReceived;
|
engine.Received -= WhenReceived;
|
||||||
engine.TransmitChanged -= WhenTransmitChanged;
|
engine.TransmitChanged -= WhenTransmitChanged;
|
||||||
engine.ConnectionChanged -= WhenConnectionChanged;
|
engine.ConnectionChanged -= WhenConnectionChanged;
|
||||||
@@ -154,12 +185,24 @@ public sealed partial class DigitalWindow : RefreshableWindow
|
|||||||
Print(text);
|
Print(text);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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.
|
||||||
private void WhenTransmitChanged(object? sender, bool transmitting) =>
|
private void WhenTransmitChanged(object? sender, bool transmitting) =>
|
||||||
Dispatcher.UIThread.Post(() =>
|
Dispatcher.UIThread.Post(() =>
|
||||||
{
|
{
|
||||||
TransmitDot.Background = transmitting
|
TransmitDot.Background = transmitting
|
||||||
? Themes.Brush(Themes.Current.TransmitLight)
|
? Themes.Brush(Themes.Current.TransmitLight)
|
||||||
: Brushes.Transparent;
|
: Brushes.Transparent;
|
||||||
|
if (!transmitting && buffer is { IsSending: false })
|
||||||
|
{
|
||||||
|
buffer.Clear();
|
||||||
|
ShowBuffer();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
private void WhenConnectionChanged(object? sender, bool connected) =>
|
private void WhenConnectionChanged(object? sender, bool connected) =>
|
||||||
@@ -525,3 +568,18 @@ public sealed partial class DigitalWindow : RefreshableWindow
|
|||||||
_ => StackOrder.Disabled,
|
_ => 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();
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user