using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; using Nonemm.App.Configuration; using Nonemm.App.Dialogs; using Nonemm.Contests.Rules; using Nonemm.App.Theming; using Nonemm.Core; using Nonemm.Session; namespace Nonemm.App.Windows; /// Taking QTC traffic down, or reading it out. Laid out as N1MM lays it out: a /// header, ten lines of time, callsign and serial number, an Agn and a Cfm /// button beside each, and Clear, Close and Cancel underneath. /// /// Receiving, the lines are typed as they arrive. Sending, they are filled from /// the log and cannot be edited — what is being reported is what was worked. /// /// On CW it puts the traffic on the air through the entry window's keyer, the /// way N1MM's QTC window sends through its own entry window. On RTTY it sends /// through the digital engine the same way, but a line at a time rather than a /// field at a time, and Send All reads the whole series out in one message. On /// SSB nothing is sent: that needs a voice keyer, and there is none here. public sealed partial class QtcWindow : Window { private static IBrush Saved => Themes.Brush(Themes.Current.GoodBackground); private static IBrush Unsaved => Themes.Brush(Themes.Current.EmptyBackground); private static IBrush Malformed => Themes.Brush(Themes.Current.BadBackground); private readonly AppSession session; private readonly OperatingPosition position; private readonly QtcTraffic traffic; private readonly Callsign station; private readonly Func send; private readonly Action refresh; private bool isSending; /// How many lines are live. Receiving, the header says: `4/5` is series 4 /// with five QTCs in it, so five lines are enabled and the rest are not. /// It is capped by how many of the station's ten are still free, which is /// what N1MM's `ColorizeCWRXFields` does with it. private int activeLines; /// The line last read out, which is the one the number keys repeat a field /// of. private int lastSent; private readonly List rows = []; private readonly List ready = []; /// `send` puts a message on the air through the entry window this was /// opened from, so QTC traffic goes out the same way a function key does. public QtcWindow( AppSession session, OperatingPosition position, Callsign station, QtcDirection direction, Func send, Action refresh) { this.session = session; this.position = position; this.station = station; this.send = send; this.refresh = refresh; isSending = direction == QtcDirection.Send; traffic = new QtcTraffic(position); InitializeComponent(); // on RTTY traffic goes both ways, so the operator says which this is DirectionPanel.IsVisible = direction == QtcDirection.Either; SendButton.IsChecked = isSending; ReceiveButton.IsChecked = !isSending; SendButton.IsCheckedChanged += (_, _) => SetDirection(SendButton.IsChecked == true); Reset(); AddHandler(KeyDownEvent, OnWindowKeyDown, RoutingStrategies.Tunnel); HeaderBox.LostFocus += (_, _) => ApplyHeaderLimit(); Closed += (_, _) => { ShowCall(null); session.QtcLines = null; }; // focus set before the window is on screen does not stick Opened += (_, _) => FirstBox().Focus(); } /// Where the cursor goes: RX Ready when the operator wants that step, the /// header when it has to be typed, the first line when it is filled in. private Control FirstBox() => isSending ? rows[0].Time : session.Settings.QtcSkipReady ? HeaderBox : ReadyButton; /// N1MM writes the number keys under the lines. They only do anything on /// CW, so the line says something else on the other modes. private string KeysLine() { if (!IsCw) { return isSending ? IsRtty ? "Send All reads the whole series out — Snd n sends one QTC again" : "Nothing goes out on SSB: read the lines out yourself, then Close to log them." : "Type the header, then a line per QTC: time, callsign, serial number."; } return isSending ? "1 = time 2 = call 3 = serial 4 = header — sends that field of the line again" : "Shift 1 = ask time Shift 2 = ask call Shift 3 = ask serial"; } /// Which mode the traffic goes out on. The contest says, not what the radio /// happens to be on, which is how N1MM decides too. private bool IsCw => position.Contest.Modes is [ModeCategory.Cw]; private bool IsRtty => position.Contest.Modes.Contains(ModeCategory.Digital); /// Whether this window puts anything on the air. SSB needs a voice keyer /// and there is none here, so on SSB the operator speaks and the window /// only writes the traffic down. private bool Keys => IsCw || IsRtty; private Settings Settings => session.Settings; /// The header of the series as it goes out in a message, which is N1MM's /// `QTC 3/10`. The box shows the station callsign after it on RTTY, and /// that is for the operator rather than for the air. private string SeriesHeader => ready.Count > 0 ? $"QTC {ready[0].SeriesText}" : ""; private void SetDirection(bool sending) { if (sending == isSending) { return; } isSending = sending; Reset(); FirstBox().Focus(); } /// Everything that follows from which way the traffic goes. private void Reset() { // taking traffic down on RTTY, a line clicked in the digital window // fills the next row, which is how the series is copied there session.QtcLines = !isSending && IsRtty ? TakeLine : null; Title = isSending ? $"Send QTC · {station.Text}" : $"Receive QTC · {station.Text}"; CountsText.Text = traffic.CountsFor(station).Text; KeysText.Text = KeysLine(); // N1MM's labels: the buttons say what they do in the direction the // traffic is going, and the ones with nothing to do are hidden ReadyButton.IsVisible = Keys || !isSending; ReadyButton.Content = !IsCw ? isSending ? "R U QRV" : "RX Ready" : isSending ? "R U QRV" : "QRV"; HeaderAgainButton.Content = isSending ? "Snd Hdr" : "Hdr Agn"; HeaderAgainButton.IsVisible = !isSending || Keys; // N1MM's All Agn, which asks for the whole series again. It is one // message, so it is RTTY only AllAgainButton.IsVisible = !isSending && IsRtty; HeaderCfmButton.IsVisible = !isSending; // N1MM's Send All, which reads a whole series out in one message. It // needs the engine to hold the text, so it is RTTY only SendAllButton.IsVisible = isSending && IsRtty; ClearButton.IsVisible = !isSending; CloseButton.Content = IsCw ? "Exit" : "Close"; HeaderBox.IsReadOnly = isSending; BuildRows(); Fill(); } private void BuildRows() { Lines.ColumnDefinitions.Clear(); Lines.RowDefinitions.Clear(); Lines.Children.Clear(); rows.Clear(); foreach (string width in new[] { "60", "120", "60", "Auto", "Auto", "Auto" }) { Lines.ColumnDefinitions.Add(new ColumnDefinition( width == "Auto" ? GridLength.Auto : new GridLength(double.Parse(width)))); } for (int at = 0; at < WaeQtc.MostPerSeries; at++) { Lines.RowDefinitions.Add(new RowDefinition(GridLength.Auto)); TextBox time = Box(at, 0); TextBox call = Box(at, 1); TextBox number = Box(at, 2); Button again = new() { Content = isSending && (IsCw || IsRtty) ? $"Snd{at + 1}" : $"Agn{at + 1}", Margin = new Avalonia.Thickness(2, 1, 2, 1), }; Button confirm = new() { Content = $"Cfm{at + 1}", Margin = new Avalonia.Thickness(2, 1, 2, 1) }; Button skip = new() { Content = "S", Margin = new Avalonia.Thickness(2, 1, 2, 1) }; int line = at; again.Click += (_, _) => OnLineAgain(line); confirm.Click += (_, _) => OnLineConfirm(line); skip.Click += (_, _) => OnSkip(line); // nothing to confirm while reading our own log out, which is how // N1MM hides the column confirm.IsVisible = !isSending; // N1MM's skip button, for a line that arrived too broken to read. // It belongs to the RTTY way of working, where the lines come in // one transmission and the rows have to stay in step with them skip.IsVisible = !isSending && IsRtty; Grid.SetRow(again, at); Grid.SetColumn(again, 3); Grid.SetRow(confirm, at); Grid.SetColumn(confirm, 4); Grid.SetRow(skip, at); Grid.SetColumn(skip, 5); Lines.Children.Add(again); Lines.Children.Add(confirm); Lines.Children.Add(skip); rows.Add(new Row(time, call, number, again, confirm, skip)); } } private TextBox Box(int row, int column) { TextBox box = new() { FontFamily = new FontFamily("monospace"), Margin = new Avalonia.Thickness(2, 1, 2, 1), IsReadOnly = isSending, }; box.TextChanged += (_, _) => Paint(); if (!isSending && column == 0) { int at = row; box.LostFocus += (_, _) => FillTime(at); } if (!isSending && column == 1) { box.TextChanged += (_, _) => ShowCall(box.Text ?? ""); box.GotFocus += (_, _) => ShowCall(box.Text ?? ""); box.LostFocus += (_, _) => ShowCall(null); } Grid.SetRow(box, row); Grid.SetColumn(box, column); Lines.Children.Add(box); return box; } /// A time typed short is filled in from the line above as the cursor /// leaves the box, which is where N1MM does it. private void FillTime(int at) { string above = at > 0 ? rows[at - 1].Time.Text ?? "" : ""; string filled = QtcTraffic.FillTime(rows[at].Time.Text ?? "", above); if (filled != (rows[at].Time.Text ?? "")) { rows[at].Time.Text = filled; } } /// Points the check window at the callsign being typed here rather than /// the one in the entry window, and back again when the cursor leaves. /// N1MM leaves the check window on the entry window's call. private void ShowCall(string? call) { if (session.Check is null) { return; } session.Check.Elsewhere = call; refresh(); } /// Sending, the header and the lines come from the log. Receiving, both are /// empty and the operator fills them in. private void Fill() { ready.Clear(); if (!isSending) { HeaderBox.Text = ""; // until a header says otherwise, every line the station has left activeLines = Math.Min(traffic.Remaining(station), rows.Count); for (int at = 0; at < rows.Count; at++) { rows[at].SetEnabled(at < activeLines); } Paint(); return; } ready.AddRange(traffic.ToSend(station, session.Settings.QtcLinesPerSeries)); activeLines = ready.Count; HeaderBox.Text = ready.Count == 0 ? "" : IsRtty ? $"{SeriesHeader} - {station.Text}" : SeriesHeader; for (int at = 0; at < rows.Count; at++) { bool has = at < ready.Count; rows[at].Time.Text = has ? ready[at].TimeUtc : ""; rows[at].Call.Text = has ? ready[at].Call.Text : ""; rows[at].Number.Text = has ? $"{ready[at].Number:00}" : ""; rows[at].SetEnabled(has); } if (ready.Count == 0) { StatusText.Text = "nothing left to report"; } Paint(); } /// A line of received text the operator clicked in the digital window. The /// header goes in the header box, and a line goes in the first row that is /// still to come, so the rows stay in step with the series even when a /// line was skipped. False when the line is not a QTC or there is no room /// left, and then the digital window treats the click as it always does. private bool TakeLine(string text) { if ((HeaderBox.Text ?? "").Trim().Length == 0 && QtcTraffic.ReadHeader(text) is not null) { HeaderBox.Text = text.Trim().ToUpperInvariant(); ApplyHeaderLimit(); Paint(); return true; } if (QtcTraffic.SplitLine(text) is not { } line) { return false; } for (int at = 0; at < rows.Count && at < activeLines; at++) { if (!rows[at].IsEmpty || rows[at].IsStruck) { continue; } rows[at].Time.Text = line.Time; rows[at].Call.Text = line.Call; rows[at].Number.Text = line.Number; Paint(); return true; } return false; } /// Narrows the live lines to what the header asks for. `4/5` is series 4 /// with five QTCs in it, so five lines take traffic and the other five are /// switched off. A station with fewer than five of its ten left gets that /// many, which is the cap N1MM applies in `ColorizeCWRXFields`. Nothing /// already typed is cleared. private void ApplyHeaderLimit() { if (isSending) { return; } int free = Math.Min(traffic.Remaining(station), rows.Count); int wanted = QtcTraffic.ReadHeader(HeaderBox.Text ?? "") is { } header ? header.Count : free; activeLines = Math.Clamp(Math.Min(wanted, free), 0, rows.Count); for (int at = 0; at < rows.Count; at++) { rows[at].SetEnabled(at < activeLines); } Paint(); } /// Green when the line is ready to save, yellow when it is filled in but /// cannot be read, red while it is still empty. private void Paint() { (int Series, int Count)? header = QtcTraffic.ReadHeader(HeaderBox.Text ?? ""); HeaderBox.Background = header is null ? (HeaderBox.Text ?? "").Trim().Length == 0 ? Unsaved : Malformed : Saved; int good = 0; for (int at = 0; at < rows.Count; at++) { Row row = rows[at]; bool empty = row.IsEmpty; WaeQtc? line = empty || header is null ? null : ReadLine(at, header.Value); row.Paint(at >= activeLines ? null : line is not null ? Saved : empty ? Unsaved : Malformed); if (line is not null) { good++; } } StatusText.Text = $"{good} of {header?.Count ?? 0}"; } private WaeQtc? ReadLine(int at, (int Series, int Count) header) => QtcTraffic.Read( station, isSending, header.Series, header.Count, rows[at].Time.Text ?? "", rows[at].Call.Text ?? "", rows[at].Number.Text ?? ""); /// Tab, enter and space all move to the next box the operator types in, /// and shift and tab move back. N1MM handles the three keys itself in /// every box rather than leaving them to the tab order, so the walk never /// lands on a switched-off line or on a button beside one. private void OnWindowKeyDown(object? sender, KeyEventArgs e) { switch (e.Key) { case Key.Escape: e.Handled = true; Close(false); break; case Key.Tab: e.Handled = true; Move(e.KeyModifiers.HasFlag(KeyModifiers.Shift) ? -1 : 1); break; case >= Key.D1 and <= Key.D4 when IsCw && isSending: case >= Key.NumPad1 and <= Key.NumPad4 when IsCw && isSending: e.Handled = true; SendField(FieldOf(e.Key)); break; case >= Key.D1 and <= Key.D3 when IsCw && e.KeyModifiers.HasFlag(KeyModifiers.Shift): case >= Key.NumPad1 and <= Key.NumPad3 when IsCw && e.KeyModifiers.HasFlag(KeyModifiers.Shift): e.Handled = true; AskFieldAgain(FieldOf(e.Key)); break; case Key.Enter when e.Source is TextBox: case Key.Space when e.Source is TextBox: e.Handled = true; Move(1); break; } } /// The boxes and buttons the operator works through, in order and without /// the ones that are switched off: the header, then each live line, and /// Close at the end. Reading traffic out there is nothing to type, so the /// walk is over the send buttons instead. private List WalkOrder() { List order = []; if (!isSending) { order.Add(HeaderBox); } for (int at = 0; at < rows.Count && at < activeLines; at++) { Row row = rows[at]; if (isSending) { order.Add(row.Again); continue; } order.Add(row.Time); order.Add(row.Call); order.Add(row.Number); if (!session.Settings.QtcSkipConfirm && row.Confirm.IsVisible) { order.Add(row.Confirm); } } order.Add(CloseButton); return order; } /// One step along the walk. Past the last box the cursor lands on Close, /// which is where N1MM leaves it; before the first it stays put. private void Move(int by) { List order = WalkOrder(); int at = order.FindIndex(b => b.IsFocused); if (at < 0) { order[0].Focus(); return; } for (int next = at + by; next >= 0 && next < order.Count; next += by) { if (order[next].IsEffectivelyEnabled) { order[next].Focus(); return; } } if (by > 0) { CloseButton.Focus(); } } /// Reading traffic out, this asks the other station whether it is ready; /// taking it down, it says we are. N1MM keys `QRV?` and `QRV` and plays a /// recording on SSB, which there is no keyer for here. private void OnReady(object? sender, RoutedEventArgs e) { if (IsCw) { _ = send(isSending ? QtcMessages.AreYouReady : QtcMessages.ReadyToReceive); } else if (IsRtty) { _ = send(QtcMessages.Rtty( isSending ? Settings.QtcRttyAreYouReady : Settings.QtcRttyReady)); } Control next = isSending ? SendAllButton.IsVisible ? SendAllButton : rows[0].Again : HeaderBox; next.Focus(); } /// One button, as N1MM has it: reading traffic out it keys the header of /// the series, taking it down it asks for the header again. private void OnHeaderAgain(object? sender, RoutedEventArgs e) { if (isSending) { // the box shows the station callsign after the header on RTTY, and // that is for the operator rather than for the air _ = send(IsRtty ? QtcMessages.Rtty(SeriesHeader) : HeaderBox.Text ?? ""); rows[0].Again.Focus(); return; } if (IsCw) { _ = send(QtcMessages.HeaderAgain); } else if (IsRtty) { _ = send(QtcMessages.Rtty(QtcMessages.RttyHeaderAgain)); } if (session.Settings.QtcHeaderAgainClears) { HeaderBox.Text = ""; } HeaderBox.Focus(); } private void OnHeaderConfirm(object? sender, RoutedEventArgs e) => rows[0].Time.Focus(); /// N1MM's All Agn: asks for the header and every line of the series again, /// and clears what was taken down so the series can be copied afresh. private void OnAllAgain(object? sender, RoutedEventArgs e) { _ = send(QtcMessages.Rtty(Settings.QtcRttyAllAgain)); foreach (Row row in rows) { row.Clear(); } HeaderBox.Focus(); Paint(); } /// Ask for a line again: N1MM sends its Agn message and comes back to the /// line. Here it clears the line and puts the cursor in it. private void OnLineAgain(int at) { if (isSending) { if (IsCw || IsRtty) { SendLine(at); return; } rows[at].Time.Focus(); return; } // N1MM asks for the line by number — `AGN QTC: 5 5 5` on RTTY, `AGN // QTC 5` on CW — and the RTTY message always clears the line, because // the line is copied again from the top if (IsRtty) { _ = send(QtcMessages.Rtty( QtcMessages.LineAgain(Settings.QtcRttyLineAgain, at + 1), Settings.QtcRttySpacing)); rows[at].Clear(); } else { if (IsCw) { _ = send($"AGN QTC {at + 1}"); } if (Settings.QtcAgainClearsLine) { rows[at].Clear(); } } rows[at].Time.Focus(); Paint(); } /// Reads one line out and moves to the next: the time, the callsign and the /// serial number, with the operator's spacing between them. When the last /// line has gone the cursor lands on Close, which sends the TU message. private void SendLine(int at) { lastSent = at; _ = send(IsRtty ? QtcMessages.SendOne(session.Settings.QtcRttySpacing, RttyLine(at)) : QtcMessages.Line( rows[at].Time.Text ?? "", rows[at].Call.Text ?? "", rows[at].Number.Text ?? "", session.Settings.QtcCwFieldSpacing)); for (int next = at + 1; next < rows.Count; next++) { if (rows[next].Again.IsEffectivelyEnabled) { rows[next].Again.Focus(); return; } } CloseButton.Focus(); } private string RttyLine(int at) => QtcMessages.RttyLine( rows[at].Time.Text ?? "", rows[at].Call.Text ?? "", rows[at].Number.Text ?? ""); /// N1MM's Send All: the whole series in one message. RTTY runs at 45 baud, /// so a series is the best part of a minute of transmission, and sending it /// a line at a time means the operator presses a button between each one /// while the transmitter is up. The heading, the spacing between the lines /// and the ending are the operator's, and the message keys the transmitter /// and drops it again itself. private void OnSendAll(object? sender, RoutedEventArgs e) { List lines = []; for (int at = 0; at < rows.Count; at++) { if (!rows[at].IsEmpty) { lines.Add(RttyLine(at)); } } string message = QtcMessages.SendAll( session.Settings.QtcRttySendAllHeading, session.Settings.QtcRttySendAllEnding, session.Settings.QtcRttySpacing, SeriesHeader, lines); if (message.Length == 0) { StatusText.Text = "nothing to send"; return; } lastSent = lines.Count - 1; _ = send(message); CloseButton.Focus(); } /// 1, 2, 3 and 4 as N1MM numbers them: the time, the call, the serial /// number and the header. private static int FieldOf(Key key) => key is >= Key.NumPad1 and <= Key.NumPad4 ? key - Key.NumPad1 : key - Key.D1; /// Reading traffic out, the number keys send one field of the line last /// sent again, for a station that missed a piece of it. private void SendField(int field) { if (field == 3) { _ = send(HeaderBox.Text ?? ""); return; } Row row = rows[Math.Max(0, lastSent)]; _ = send((field switch { 0 => row.Time.Text, 1 => row.Call.Text, _ => row.Number.Text, } ?? "").Trim()); } /// Taking traffic down, shift and a number ask for that field again and /// clear it. One message per field, all three in the settings. private void AskFieldAgain(int field) { _ = send(field switch { 0 => session.Settings.QtcCwTimeAgain, 1 => session.Settings.QtcCwCallAgain, _ => session.Settings.QtcCwNumberAgain, }); if (FocusedRow() is { } row) { TextBox box = field switch { 0 => row.Time, 1 => row.Call, _ => row.Number, }; box.Text = ""; box.Focus(); Paint(); } } /// The line the cursor is in, or the first one when it is somewhere else. private Row? FocusedRow() => rows.FirstOrDefault(r => r.Time.IsFocused || r.Call.IsFocused || r.Number.IsFocused) ?? rows.FirstOrDefault(); /// N1MM's S button: the line was too broken to read, so it is struck /// through and the ones after it keep their places. Agn n clears the /// strike and asks for the line again. private void OnSkip(int at) { rows[at].Skip(); if (at + 1 < rows.Count && at + 1 < activeLines) { rows[at + 1].Time.Focus(); } Paint(); } private void OnLineConfirm(int at) { Control next = at + 1 < rows.Count ? rows[at + 1].Time : CloseButton; next.Focus(); } private void OnClear(object? sender, RoutedEventArgs e) { foreach (Row row in rows) { row.Clear(); } Fill(); } /// Close saves what is filled in, which is what N1MM's Close does. A line /// that cannot be read is left out rather than guessed at. private void OnClose(object? sender, RoutedEventArgs e) { if (QtcTraffic.ReadHeader(HeaderBox.Text ?? "") is not { } header) { StatusText.Text = "the header is not a series"; HeaderBox.Focus(); return; } List lines = []; for (int at = 0; at < rows.Count; at++) { if (!rows[at].IsEmpty && ReadLine(at, header) is { } line) { lines.Add(isSending && at < ready.Count ? ready[at] with { Series = header.Series } : line); } } Save(lines); // N1MM sends a message as the window closes: the TU on CW, and on RTTY // `R R R ALL OK BK` from the station that took the traffic down or // `GL DE ` from the one that read it out if (IsCw && Settings.QtcCwTu.Trim().Length > 0) { _ = send(QtcMessages.Tu(Settings.QtcCwTu, HeaderBox.Text ?? "")); } else if (IsRtty) { _ = send(QtcMessages.Rtty( isSending ? Settings.QtcRttySendSave : Settings.QtcRttySave)); } Close(true); } private void OnCancel(object? sender, RoutedEventArgs e) => Close(false); private async void OnSetup(object? sender, RoutedEventArgs e) { QtcSetupDialog dialog = new(session.Settings); if (await dialog.ShowDialog(this) is { } updated) { session.Save(updated); Reset(); FirstBox().Focus(); } } /// One row per line, a second apart, so they keep their order in the log. /// That is what N1MM does with them too. private void Save(IReadOnlyList lines) { DateTime at = DateTime.UtcNow; at = at.AddTicks(-(at.Ticks % TimeSpan.TicksPerSecond)); for (int line = 0; line < lines.Count; line++) { Qso row = new() { Id = Qso.NewId(), TimestampUtc = at.AddSeconds(line), Call = station, Frequency = position.Frequency, Mode = position.Mode, RadioNumber = position.RadioNumber, ContestName = position.Contest.Name, ContestNumber = position.Instance.ContestNumber, Operator = position.Session.Operator.Length > 0 ? position.Session.Operator : position.Me.Callsign, IsRunQso = position.IsRunning, }; position.Session.Add(lines[line].ApplyTo(CountryFields.Apply(row, session.Countries))); } } private sealed record Row( TextBox Time, TextBox Call, TextBox Number, Button Again, Button Confirm, Button SkipButton) { /// The mark a skipped line carries in each of its boxes, which is /// N1MM's line through the fields. private const string Struck = "-"; /// A line that was skipped holds nothing worth logging, so it counts /// as empty: it is not saved, and the window paints it as a line still /// to come. public bool IsEmpty => IsStruck || ((Time.Text ?? "").Trim().Length == 0 && (Call.Text ?? "").Trim().Length == 0 && (Number.Text ?? "").Trim().Length == 0); public bool IsStruck => (Time.Text ?? "").Trim() == Struck; public void Skip() { Time.Text = Struck; Call.Text = Struck; Number.Text = Struck; } /// A null colour is a line that is switched off, which keeps the /// window's own background rather than any of the three. public void Paint(IBrush? colour) { foreach (TextBox box in new[] { Time, Call, Number }) { if (colour is null) { box.ClearValue(BackgroundProperty); } else { box.Background = colour; } } } public void Clear() { Time.Text = ""; Call.Text = ""; Number.Text = ""; } public void SetEnabled(bool enabled) { Time.IsEnabled = enabled; Call.IsEnabled = enabled; Number.IsEnabled = enabled; Again.IsEnabled = enabled; Confirm.IsEnabled = enabled; SkipButton.IsEnabled = enabled; } } }