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:
@@ -0,0 +1,40 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Nonemm.App.Controls">
|
||||
<Style Selector="controls|TransmitPane">
|
||||
<Setter Property="Background" Value="{DynamicResource FieldBackground}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource FieldForeground}" />
|
||||
<Setter Property="CaretBrush" Value="{DynamicResource FieldForeground}" />
|
||||
<Setter Property="SelectionBrush" Value="#3399FF" />
|
||||
<Setter Property="SelectionForegroundBrush" Value="#FFFFFF" />
|
||||
<Setter Property="Padding" Value="3,2" />
|
||||
<Setter Property="Template">
|
||||
<!-- a TextBox subclass gets no theme of its own, so the pane carries
|
||||
its own template. It is the Fluent one cut down to what the pane
|
||||
uses: no watermark, no clear button, no border of its own -->
|
||||
<ControlTemplate>
|
||||
<ScrollViewer Name="PART_ScrollViewer"
|
||||
Background="{TemplateBinding Background}"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<controls:TransmitPresenter Name="PART_TextPresenter"
|
||||
Text="{TemplateBinding Text}"
|
||||
CaretIndex="{TemplateBinding CaretIndex}"
|
||||
SelectionStart="{TemplateBinding SelectionStart}"
|
||||
SelectionEnd="{TemplateBinding SelectionEnd}"
|
||||
SelectionBrush="{TemplateBinding SelectionBrush}"
|
||||
SelectionForegroundBrush="{TemplateBinding SelectionForegroundBrush}"
|
||||
CaretBrush="{TemplateBinding CaretBrush}"
|
||||
TextAlignment="{TemplateBinding TextAlignment}"
|
||||
TextWrapping="{TemplateBinding TextWrapping}"
|
||||
LineHeight="{TemplateBinding LineHeight}"
|
||||
LetterSpacing="{TemplateBinding LetterSpacing}"
|
||||
SentLength="{TemplateBinding SentLength}"
|
||||
SentBrush="{TemplateBinding SentBrush}"
|
||||
VerticalAlignment="Top" />
|
||||
</ScrollViewer>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
</Styles>
|
||||
@@ -0,0 +1,37 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace Nonemm.App.Controls;
|
||||
|
||||
/// The digital transmit pane: one editable box in which the first `SentLength`
|
||||
/// characters — what has already gone to the engine — are drawn in `SentBrush`.
|
||||
///
|
||||
/// Avalonia's TextBox draws all of its text in one brush, so this was a label
|
||||
/// beside a box before. That took width from the box as the label grew and the
|
||||
/// label did not wrap. A TextBox builds its text through a TextPresenter, and
|
||||
/// the TextLayout under it does take a brush per run, so the pane is a TextBox
|
||||
/// with `TransmitPresenter` in place of the plain presenter. The template in
|
||||
/// `DigitalWindow.axaml` is what puts it there.
|
||||
public class TransmitPane : TextBox
|
||||
{
|
||||
public static readonly StyledProperty<int> SentLengthProperty =
|
||||
AvaloniaProperty.Register<TransmitPane, int>(nameof(SentLength));
|
||||
|
||||
public static readonly StyledProperty<IBrush?> SentBrushProperty =
|
||||
AvaloniaProperty.Register<TransmitPane, IBrush?>(nameof(SentBrush));
|
||||
|
||||
/// How many characters at the front of the text have gone out.
|
||||
public int SentLength
|
||||
{
|
||||
get => GetValue(SentLengthProperty);
|
||||
set => SetValue(SentLengthProperty, value);
|
||||
}
|
||||
|
||||
/// What those characters are drawn in.
|
||||
public IBrush? SentBrush
|
||||
{
|
||||
get => GetValue(SentBrushProperty);
|
||||
set => SetValue(SentBrushProperty, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.Presenters;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.TextFormatting;
|
||||
using Avalonia.Utilities;
|
||||
|
||||
namespace Nonemm.App.Controls;
|
||||
|
||||
/// The presenter behind `TransmitPane`: the same text layout Avalonia builds
|
||||
/// for a TextBox, with the first `SentLength` characters given `SentBrush`.
|
||||
///
|
||||
/// TextPresenter builds its layout in `CreateTextLayout` and already passes
|
||||
/// per-run overrides for the selection, so this adds one more run to that list.
|
||||
/// The selection has to keep its own colour on top, which is why the coloured
|
||||
/// run is cut around it rather than laid over it: overlapping runs are not
|
||||
/// defined.
|
||||
public class TransmitPresenter : TextPresenter
|
||||
{
|
||||
public static readonly StyledProperty<int> SentLengthProperty =
|
||||
AvaloniaProperty.Register<TransmitPresenter, int>(nameof(SentLength));
|
||||
|
||||
public static readonly StyledProperty<IBrush?> SentBrushProperty =
|
||||
AvaloniaProperty.Register<TransmitPresenter, IBrush?>(nameof(SentBrush));
|
||||
|
||||
/// The width the layout is built to. `TextPresenter` keeps its own copy and
|
||||
/// does not hand it out, so it is read off the measure pass here.
|
||||
private Size constraint;
|
||||
|
||||
public int SentLength
|
||||
{
|
||||
get => GetValue(SentLengthProperty);
|
||||
set => SetValue(SentLengthProperty, value);
|
||||
}
|
||||
|
||||
public IBrush? SentBrush
|
||||
{
|
||||
get => GetValue(SentBrushProperty);
|
||||
set => SetValue(SentBrushProperty, value);
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
constraint = availableSize;
|
||||
return base.MeasureOverride(availableSize);
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
base.OnPropertyChanged(change);
|
||||
if (change.Property == SentLengthProperty || change.Property == SentBrushProperty)
|
||||
{
|
||||
InvalidateTextLayout();
|
||||
}
|
||||
}
|
||||
|
||||
protected override TextLayout CreateTextLayout()
|
||||
{
|
||||
string text = Text ?? "";
|
||||
int sent = Math.Clamp(SentLength, 0, text.Length);
|
||||
// nothing to colour, or a case the base class handles on its own: the
|
||||
// password character replaces the text, and a preedit run is the input
|
||||
// method's, not ours
|
||||
if (sent == 0 || SentBrush is null || PasswordChar != '\0'
|
||||
|| !string.IsNullOrEmpty(PreeditText))
|
||||
{
|
||||
return base.CreateTextLayout();
|
||||
}
|
||||
Typeface typeface = new(FontFamily, FontStyle, FontWeight, FontStretch);
|
||||
// a zero constraint is a measure with no bound, which is infinity to
|
||||
// the layout
|
||||
double width = constraint.Width > 0 ? constraint.Width : double.PositiveInfinity;
|
||||
double height = constraint.Height > 0 ? constraint.Height : double.PositiveInfinity;
|
||||
return new TextLayout(
|
||||
text,
|
||||
typeface,
|
||||
FontSize,
|
||||
Foreground,
|
||||
TextAlignment,
|
||||
TextWrapping,
|
||||
null,
|
||||
null,
|
||||
FlowDirection,
|
||||
width,
|
||||
height,
|
||||
LineHeight,
|
||||
LetterSpacing,
|
||||
0,
|
||||
FontFeatures,
|
||||
Runs(typeface, sent));
|
||||
}
|
||||
|
||||
/// The coloured runs, in order and not overlapping. The selection, when
|
||||
/// there is one with a colour of its own, cuts the coloured run in two.
|
||||
private List<ValueSpan<TextRunProperties>> Runs(Typeface typeface, int sent)
|
||||
{
|
||||
int from = Math.Min(SelectionStart, SelectionEnd);
|
||||
int to = Math.Max(SelectionStart, SelectionEnd);
|
||||
bool selected = ShowSelectionHighlight && to > from && SelectionForegroundBrush is not null;
|
||||
List<ValueSpan<TextRunProperties>> runs = [];
|
||||
Add(runs, 0, selected ? Math.Min(sent, from) : sent, SentBrush, typeface);
|
||||
if (selected)
|
||||
{
|
||||
Add(runs, from, to, SelectionForegroundBrush, typeface);
|
||||
Add(runs, to, sent, SentBrush, typeface);
|
||||
}
|
||||
return runs;
|
||||
}
|
||||
|
||||
private void Add(
|
||||
List<ValueSpan<TextRunProperties>> runs,
|
||||
int start,
|
||||
int end,
|
||||
IBrush? brush,
|
||||
Typeface typeface)
|
||||
{
|
||||
if (end <= start)
|
||||
{
|
||||
return;
|
||||
}
|
||||
runs.Add(new ValueSpan<TextRunProperties>(
|
||||
start,
|
||||
end - start,
|
||||
new GenericTextRunProperties(
|
||||
typeface,
|
||||
FontSize,
|
||||
null,
|
||||
brush,
|
||||
null,
|
||||
BaselineAlignment.Baseline,
|
||||
null,
|
||||
FontFeatures)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user