Fit more of the log on screen and edit a cell without fighting it
The theme gave each cell 32 pixels of height, which fits about seven contacts in the window. A row is now as tall as its text and one pixel either side, so about fifteen fit. Editing a cell: - the columns bind one way, and the value is written back when the edit is taken, on Enter or a click elsewhere. A two-way binding wrote on every key press, which took the edit as the first character was typed and closed the box. - Enter on the current cell opens the box. The handler runs before the grid's own, which reads Enter as "move down a line". - Left, Right, Home and End stay in the box. The grid read a key the box left alone as "go to the next cell", and the box leaves a key alone when the caret cannot move; End also landed at the start of the text. - the box takes the height and the left inset of the text it covers, so nothing moves as it opens. An edit no longer rebuilds the list. The lines on screen are given the contact again, so the view stays where the operator put it instead of jumping to the top and back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Nonemm.Contests;
|
using Nonemm.Contests;
|
||||||
using Nonemm.Core;
|
using Nonemm.Core;
|
||||||
@@ -9,28 +10,35 @@ namespace Nonemm.App.Windows;
|
|||||||
/// so the columns can follow the contest exchange and the operator's choice
|
/// so the columns can follow the contest exchange and the operator's choice
|
||||||
/// instead of being fixed in the XAML. The colour comes from the same scorer as
|
/// instead of being fixed in the XAML. The colour comes from the same scorer as
|
||||||
/// the entry window and the bandmap.
|
/// the entry window and the bandmap.
|
||||||
public sealed class LogRow
|
public sealed class LogRow : INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
private readonly Func<Qso, QsoField, string, bool> edit;
|
public LogRow(Qso qso, Verdict? verdict)
|
||||||
|
|
||||||
public LogRow(Qso qso, Verdict? verdict, Func<Qso, QsoField, string, bool> edit)
|
|
||||||
{
|
{
|
||||||
Qso = qso;
|
Qso = qso;
|
||||||
Verdict = verdict;
|
Verdict = verdict;
|
||||||
this.edit = edit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Qso Qso { get; }
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
public Verdict? Verdict { get; }
|
public Qso Qso { get; private set; }
|
||||||
|
|
||||||
/// `name` is a `QsoField` name. A refused edit leaves the contact alone, so
|
public Verdict? Verdict { get; private set; }
|
||||||
/// the grid reads the old value back and the cell reverts.
|
|
||||||
public string this[string name]
|
/// Takes the contact again after it was edited. The grid redraws the line
|
||||||
|
/// it is already showing, so the view stays where it was. "Item" is the
|
||||||
|
/// name the indexer below has in the metadata, and the name the binding
|
||||||
|
/// listens for; any other name is ignored.
|
||||||
|
public void Show(Qso qso, Verdict? verdict)
|
||||||
{
|
{
|
||||||
get => QsoEditor.Read(Qso, Enum.Parse<QsoField>(name));
|
Qso = qso;
|
||||||
set => edit(Qso, Enum.Parse<QsoField>(name), value);
|
Verdict = verdict;
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `name` is a `QsoField` name. Read only: an edited cell is written back by
|
||||||
|
/// `LogWindow.TakeCellEdit` when the operator takes the edit, not as the
|
||||||
|
/// text is typed.
|
||||||
|
public string this[string name] => QsoEditor.Read(Qso, Enum.Parse<QsoField>(name));
|
||||||
|
|
||||||
public IBrush Colour => Verdicts.Colour(Verdict);
|
public IBrush Colour => Verdicts.Colour(Verdict);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,9 +149,11 @@ public sealed partial class LogWindow
|
|||||||
? new DataGridLength(1, DataGridLengthUnitType.Star)
|
? new DataGridLength(1, DataGridLengthUnitType.Star)
|
||||||
: new DataGridLength(Width(column)),
|
: new DataGridLength(Width(column)),
|
||||||
MinWidth = Width(column),
|
MinWidth = Width(column),
|
||||||
Binding = column.IsEditable
|
// one way in both directions of the grid: `LogWindow.TakeCellEdit`
|
||||||
? new Binding($"[{column.Field}]") { Mode = BindingMode.TwoWay }
|
// writes an edited cell back. A two-way binding would write on
|
||||||
: new Binding($"[{column.Field}]"),
|
// every key press, and the DataGrid turns a binding without a
|
||||||
|
// mode into a two-way one
|
||||||
|
Binding = new Binding($"[{column.Field}]") { Mode = BindingMode.OneWay },
|
||||||
CustomSortComparer = By(column.Field),
|
CustomSortComparer = By(column.Field),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,14 +13,32 @@
|
|||||||
<Setter Property="FontFamily" Value="monospace" />
|
<Setter Property="FontFamily" Value="monospace" />
|
||||||
<Setter Property="FontSize" Value="12" />
|
<Setter Property="FontSize" Value="12" />
|
||||||
</Style>
|
</Style>
|
||||||
<!-- the editing box is left a little shorter than the cell, otherwise
|
<!-- the theme gives a cell 32 pixels of height, which fits about a
|
||||||
it grows the row and the whole grid shifts as editing starts -->
|
dozen rows on screen. The row is cut down to the height of its
|
||||||
|
text and one pixel either side -->
|
||||||
|
<Style Selector="DataGridRow">
|
||||||
|
<Setter Property="MinHeight" Value="0" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="DataGridCell">
|
||||||
|
<Setter Property="MinHeight" Value="0" />
|
||||||
|
<Setter Property="Padding" Value="0,1" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="DataGridColumnHeader">
|
||||||
|
<Setter Property="MinHeight" Value="0" />
|
||||||
|
</Style>
|
||||||
|
<!-- the editing box takes the same height as the text it covers,
|
||||||
|
otherwise it grows the row and the whole grid shifts as editing
|
||||||
|
starts. Its border is what would make it two pixels taller, so it
|
||||||
|
is dropped: the white background already marks the cell out. The
|
||||||
|
padding is what the cell gives its text, so the text does not move
|
||||||
|
sideways as the box opens -->
|
||||||
<Style Selector="DataGridCell TextBox">
|
<Style Selector="DataGridCell TextBox">
|
||||||
<Setter Property="FontFamily" Value="monospace" />
|
<Setter Property="FontFamily" Value="monospace" />
|
||||||
<Setter Property="FontSize" Value="12" />
|
<Setter Property="FontSize" Value="12" />
|
||||||
<Setter Property="MinHeight" Value="0" />
|
<Setter Property="MinHeight" Value="0" />
|
||||||
<Setter Property="Margin" Value="0,1" />
|
<Setter Property="Margin" Value="0" />
|
||||||
<Setter Property="Padding" Value="4,0" />
|
<Setter Property="Padding" Value="12,0" />
|
||||||
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||||
</Style>
|
</Style>
|
||||||
</Grid.Styles>
|
</Grid.Styles>
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
using Avalonia;
|
||||||
using Avalonia.Collections;
|
using Avalonia.Collections;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using Avalonia.VisualTree;
|
||||||
using Nonemm.App.Dialogs;
|
using Nonemm.App.Dialogs;
|
||||||
using Nonemm.Contests;
|
using Nonemm.Contests;
|
||||||
using Nonemm.Core;
|
using Nonemm.Core;
|
||||||
@@ -9,25 +12,32 @@ using Nonemm.Session;
|
|||||||
|
|
||||||
namespace Nonemm.App.Windows;
|
namespace Nonemm.App.Windows;
|
||||||
|
|
||||||
/// The contacts of the contest in progress, newest last. Double-click a cell to
|
/// The contacts of the contest in progress, newest last. Double-click a cell or
|
||||||
/// change it; the Delete key removes the contact. Clicking a column header
|
/// press Enter on it to change it; the Delete key removes the contact. Clicking a column header
|
||||||
/// sorts on it, right-clicking picks which columns are shown and dragging one
|
/// sorts on it, right-clicking picks which columns are shown and dragging one
|
||||||
/// moves its column; see `LogWindow.Columns.cs`.
|
/// moves its column; see `LogWindow.Columns.cs`.
|
||||||
public sealed partial class LogWindow : RefreshableWindow
|
public sealed partial class LogWindow : RefreshableWindow
|
||||||
{
|
{
|
||||||
private readonly AppSession session;
|
private readonly AppSession session;
|
||||||
private string message = "";
|
private string message = "";
|
||||||
|
/// The lines the grid is showing. They are kept and given the contact again
|
||||||
|
/// when only the values changed, so that an edit does not rebuild the list
|
||||||
|
/// and move the view.
|
||||||
|
private List<LogRow> shownRows = [];
|
||||||
private string finding = "";
|
private string finding = "";
|
||||||
|
|
||||||
public LogWindow(AppSession session)
|
public LogWindow(AppSession session)
|
||||||
{
|
{
|
||||||
this.session = session;
|
this.session = session;
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
Rows.KeyDown += OnKeyDown;
|
// before the grid's own handling: the grid takes Enter to mean "move to
|
||||||
|
// the next line", which would close the editing box as soon as it opens
|
||||||
|
Rows.AddHandler(KeyDownEvent, OnKeyDown, RoutingStrategies.Tunnel);
|
||||||
Rows.ColumnReordering += HideDragIndicator;
|
Rows.ColumnReordering += HideDragIndicator;
|
||||||
Rows.AddHandler(PointerMovedEvent, ShowDragIndicator, RoutingStrategies.Tunnel);
|
Rows.AddHandler(PointerMovedEvent, ShowDragIndicator, RoutingStrategies.Tunnel);
|
||||||
Rows.ColumnReordered += (_, _) => SaveColumnOrder();
|
Rows.ColumnReordered += (_, _) => SaveColumnOrder();
|
||||||
Rows.PreparingCellForEdit += SelectCellText;
|
Rows.PreparingCellForEdit += SelectCellText;
|
||||||
|
Rows.CellEditEnding += TakeCellEdit;
|
||||||
Rows.AddHandler(ContextRequestedEvent, OnContextRequested, RoutingStrategies.Bubble);
|
Rows.AddHandler(ContextRequestedEvent, OnContextRequested, RoutingStrategies.Bubble);
|
||||||
Colour(Rows);
|
Colour(Rows);
|
||||||
Colour(Earlier);
|
Colour(Earlier);
|
||||||
@@ -38,10 +48,42 @@ public sealed partial class LogWindow : RefreshableWindow
|
|||||||
/// typing replaces the value rather than adding to it.
|
/// typing replaces the value rather than adding to it.
|
||||||
private static void SelectCellText(object? sender, DataGridPreparingCellForEditEventArgs e)
|
private static void SelectCellText(object? sender, DataGridPreparingCellForEditEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.EditingElement is TextBox box)
|
if (e.EditingElement is not TextBox box)
|
||||||
{
|
{
|
||||||
box.SelectAll();
|
return;
|
||||||
}
|
}
|
||||||
|
box.SelectAll();
|
||||||
|
// the grid hands the box back for the next cell, so the handler is
|
||||||
|
// taken off before it is put on again
|
||||||
|
box.RemoveHandler(KeyDownEvent, KeepCaretKeys);
|
||||||
|
box.AddHandler(
|
||||||
|
KeyDownEvent,
|
||||||
|
KeepCaretKeys,
|
||||||
|
RoutingStrategies.Direct | RoutingStrategies.Bubble,
|
||||||
|
handledEventsToo: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Left, Right, Home and End stay in the editing box. The box marks a key
|
||||||
|
/// as handled only when it moves the caret, and the grid reads a key the
|
||||||
|
/// box left alone as "go to the next cell". Opening the box leaves the
|
||||||
|
/// caret at the end of the text, so one press of Right closed the box.
|
||||||
|
private static void KeepCaretKeys(object? sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key is not (Key.Left or Key.Right or Key.Home or Key.End))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// the box drops a selection by putting the caret at the start of it,
|
||||||
|
// and does so after End has moved the caret to the end of the text, so
|
||||||
|
// End on a box whose text is all selected lands at the start
|
||||||
|
if (e.Key == Key.End && !e.KeyModifiers.HasFlag(KeyModifiers.Shift) && sender is TextBox box)
|
||||||
|
{
|
||||||
|
int end = (box.Text ?? "").Length;
|
||||||
|
box.CaretIndex = end;
|
||||||
|
box.SelectionStart = end;
|
||||||
|
box.SelectionEnd = end;
|
||||||
|
}
|
||||||
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnContextRequested(object? sender, ContextRequestedEventArgs e)
|
private void OnContextRequested(object? sender, ContextRequestedEventArgs e)
|
||||||
@@ -55,11 +97,13 @@ public sealed partial class LogWindow : RefreshableWindow
|
|||||||
|
|
||||||
private ContextMenu RowMenu()
|
private ContextMenu RowMenu()
|
||||||
{
|
{
|
||||||
MenuItem edit = new() { Header = "Edit contact…", InputGesture = new KeyGesture(Key.Enter) };
|
MenuItem edit = new() { Header = "Edit contact…" };
|
||||||
edit.Click += OnEditContact;
|
edit.Click += OnEditContact;
|
||||||
|
MenuItem operators = new() { Header = "Change Operator…" };
|
||||||
|
operators.Click += OnChangeOperator;
|
||||||
MenuItem delete = new() { Header = "Delete contact", InputGesture = new KeyGesture(Key.Delete) };
|
MenuItem delete = new() { Header = "Delete contact", InputGesture = new KeyGesture(Key.Delete) };
|
||||||
delete.Click += OnDelete;
|
delete.Click += OnDelete;
|
||||||
return new ContextMenu { ItemsSource = new List<MenuItem> { edit, delete } };
|
return new ContextMenu { ItemsSource = new List<MenuItem> { edit, operators, delete } };
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Refresh()
|
public override void Refresh()
|
||||||
@@ -72,19 +116,108 @@ public sealed partial class LogWindow : RefreshableWindow
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
BuildColumns(session.Logging.Editor);
|
BuildColumns(session.Logging.Editor);
|
||||||
List<LogRow> rows = session.Logging.Log.Qsos
|
IReadOnlyList<Qso> qsos = session.Logging.Log.Qsos;
|
||||||
.Select(q => new LogRow(q, VerdictFor(q), Edit))
|
if (SameContacts(qsos))
|
||||||
.ToList();
|
|
||||||
Rows.ItemsSource = Sorted(rows);
|
|
||||||
if (rows.Count > 0)
|
|
||||||
{
|
{
|
||||||
Rows.ScrollIntoView(rows[^1], null);
|
for (int at = 0; at < qsos.Count; at++)
|
||||||
|
{
|
||||||
|
shownRows[at].Show(qsos[at], VerdictFor(qsos[at]));
|
||||||
|
}
|
||||||
|
Recolour(Rows);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bool grown = qsos.Count > shownRows.Count;
|
||||||
|
Qso? bottom = BottomOfView();
|
||||||
|
shownRows = qsos.Select(q => new LogRow(q, VerdictFor(q))).ToList();
|
||||||
|
Rows.ItemsSource = Sorted(shownRows);
|
||||||
|
if (grown && shownRows.Count > 0)
|
||||||
|
{
|
||||||
|
Rows.ScrollIntoView(shownRows[^1], null);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ShowAgain(bottom, shownRows);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ShowEarlier();
|
ShowEarlier();
|
||||||
SummaryText.Text = message.Length > 0 ? message : Summary(session.Logging);
|
SummaryText.Text = message.Length > 0 ? message : Summary(session.Logging);
|
||||||
message = "";
|
message = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True when the log holds the same contacts, in the same order, as the
|
||||||
|
/// grid is showing. An edit leaves the list itself alone; logging or
|
||||||
|
/// deleting a contact does not.
|
||||||
|
private bool SameContacts(IReadOnlyList<Qso> qsos)
|
||||||
|
{
|
||||||
|
if (qsos.Count != shownRows.Count)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int at = 0; at < qsos.Count; at++)
|
||||||
|
{
|
||||||
|
if (qsos[at].Id != shownRows[at].Qso.Id)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The colour of a line is set as the grid draws it, so a line that is
|
||||||
|
/// already on screen keeps the colour of the verdict it had before the
|
||||||
|
/// edit until it is coloured again.
|
||||||
|
private static void Recolour(DataGrid grid)
|
||||||
|
{
|
||||||
|
foreach (DataGridRow row in grid.GetVisualDescendants().OfType<DataGridRow>())
|
||||||
|
{
|
||||||
|
if (row.DataContext is LogRow item)
|
||||||
|
{
|
||||||
|
row.Foreground = item.Colour;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The contact drawn at the foot of the view, or null when the log is empty
|
||||||
|
/// or has not been drawn yet. Setting `ItemsSource` puts the view back at
|
||||||
|
/// the top, so this is what the view is wound back to afterwards.
|
||||||
|
private Qso? BottomOfView()
|
||||||
|
{
|
||||||
|
Qso? found = null;
|
||||||
|
double lowest = double.MinValue;
|
||||||
|
foreach (DataGridRow row in Rows.GetVisualDescendants().OfType<DataGridRow>())
|
||||||
|
{
|
||||||
|
Point? foot = row.TranslatePoint(new Point(0, row.Bounds.Height), Rows);
|
||||||
|
if (row.DataContext is not LogRow item
|
||||||
|
|| foot is null
|
||||||
|
|| foot.Value.Y > Rows.Bounds.Height + 1
|
||||||
|
|| foot.Value.Y <= lowest)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
lowest = foot.Value.Y;
|
||||||
|
found = item.Qso;
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scrolling a row into view from the top of the list leaves it at the foot
|
||||||
|
/// of the view, which is where it was before the list was rebuilt. The grid
|
||||||
|
/// puts the view back at the top itself as it takes the new list, so this
|
||||||
|
/// waits for that to be over.
|
||||||
|
private void ShowAgain(Qso? bottom, IReadOnlyList<LogRow> rows)
|
||||||
|
{
|
||||||
|
if (bottom is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LogRow? row = rows.FirstOrDefault(r => r.Qso.Id == bottom.Id);
|
||||||
|
if (row is not null)
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() => Rows.ScrollIntoView(row, null), DispatcherPriority.Loaded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The pane under the log: what has already been worked with the call being
|
/// The pane under the log: what has already been worked with the call being
|
||||||
/// typed, so an apparent dupe can be read off the screen.
|
/// typed, so an apparent dupe can be read off the screen.
|
||||||
private void ShowEarlier()
|
private void ShowEarlier()
|
||||||
@@ -100,7 +233,7 @@ public sealed partial class LogWindow : RefreshableWindow
|
|||||||
session.Logging.Log.Qsos,
|
session.Logging.Log.Qsos,
|
||||||
position.Entry.Call,
|
position.Entry.Call,
|
||||||
position.Entry.ValueOf(ExchangeSlot.GridSquare))
|
position.Entry.ValueOf(ExchangeSlot.GridSquare))
|
||||||
.Select(q => new LogRow(q, VerdictFor(q), Edit))
|
.Select(q => new LogRow(q, VerdictFor(q)))
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,21 +277,38 @@ public sealed partial class LogWindow : RefreshableWindow
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// True when the edit was taken. A refused edit puts the reason in the
|
/// The cell is written back when the operator takes the edit — Enter, or a
|
||||||
/// summary line and the cell falls back to what it held before.
|
/// click on another cell — and not before. The columns bind one way, so
|
||||||
private bool Edit(Qso qso, QsoField field, string text)
|
/// typing changes the box alone: Escape drops what was typed, and the value
|
||||||
|
/// does not go into the log one character at a time.
|
||||||
|
private void TakeCellEdit(object? sender, DataGridCellEditEndingEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.EditAction != DataGridEditAction.Commit
|
||||||
|
|| e.EditingElement is not TextBox box
|
||||||
|
|| e.Row.DataContext is not LogRow row
|
||||||
|
|| !columnOf.TryGetValue(e.Column, out QsoColumn? column))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
string text = box.Text ?? "";
|
||||||
|
// the edit rebuilds the rows, which the grid cannot take while it is
|
||||||
|
// still closing the editing box
|
||||||
|
Dispatcher.UIThread.Post(() => Edit(row.Qso, column.Field, text));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A refused edit puts the reason in the summary line and the cell falls
|
||||||
|
/// back to what it held before.
|
||||||
|
private void Edit(Qso qso, QsoField field, string text)
|
||||||
{
|
{
|
||||||
if (session.Logging is null)
|
if (session.Logging is null)
|
||||||
{
|
{
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
QsoEdit edit = session.Logging.Edit(qso.Id, field, text);
|
QsoEdit edit = session.Logging.Edit(qso.Id, field, text);
|
||||||
if (edit.IsAccepted)
|
if (!edit.IsAccepted)
|
||||||
{
|
{
|
||||||
return true;
|
SummaryText.Text = edit.Error;
|
||||||
}
|
}
|
||||||
SummaryText.Text = edit.Error;
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnKeyDown(object? sender, KeyEventArgs e)
|
private void OnKeyDown(object? sender, KeyEventArgs e)
|
||||||
@@ -175,7 +325,7 @@ public sealed partial class LogWindow : RefreshableWindow
|
|||||||
break;
|
break;
|
||||||
case Key.Enter:
|
case Key.Enter:
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
OnEditContact(sender, new RoutedEventArgs());
|
Rows.BeginEdit();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,6 +339,39 @@ public sealed partial class LogWindow : RefreshableWindow
|
|||||||
await new EditContactDialog(session.Logging, row.Qso.Id).ShowDialog(this);
|
await new EditContactDialog(session.Logging, row.Qso.Id).ShowDialog(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Writes one callsign over the operator of every contact picked in the
|
||||||
|
/// grid. A multi-operator station logs a whole shift under whoever was at
|
||||||
|
/// the radio, so a shift logged under the wrong callsign is put right in
|
||||||
|
/// one go rather than a contact at a time.
|
||||||
|
private async void OnChangeOperator(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (session.Logging is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<Qso> picked = [.. Rows.SelectedItems.OfType<LogRow>().Select(r => r.Qso)];
|
||||||
|
if (picked.Count == 0)
|
||||||
|
{
|
||||||
|
message = "pick the contacts to change first";
|
||||||
|
Refresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
string was = picked[0].Operator.Length > 0 ? picked[0].Operator : session.Settings.CurrentOperator;
|
||||||
|
List<string> known = [.. session.Settings.Operators.Select(o => o.Callsign)];
|
||||||
|
if (await new ChangeOperatorDialog(picked.Count, was, known).ShowDialog<string?>(this) is not { } callsign)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
foreach (Qso qso in picked)
|
||||||
|
{
|
||||||
|
session.Logging.Edit(qso.Id, QsoField.Operator, callsign);
|
||||||
|
}
|
||||||
|
message = picked.Count == 1
|
||||||
|
? $"1 contact logged by {callsign}"
|
||||||
|
: $"{picked.Count} contacts logged by {callsign}";
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
private async void OnDelete(object? sender, RoutedEventArgs e)
|
private async void OnDelete(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (session.Logging is null || Rows.SelectedItem is not LogRow row)
|
if (session.Logging is null || Rows.SelectedItem is not LogRow row)
|
||||||
|
|||||||
Reference in New Issue
Block a user