Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Controls/HistoryWorkspace.Actions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ private async void ExecuteAction(string action)
{
if (SelectedHistoryDevice is { } saved)
{
var dialog = new HomeAssistantAdviceWindow(HomeAssistantText.FromHistory(saved), WebLink.Open, value => Clipboard.SetText(value));
var dialog = new HomeAssistantAdviceWindow(HomeAssistantText.FromHistory(saved), WebLink.Open,
value => Clipboard.SetText(value), surveyStore: SurveySource(StoreFor(_entry)));
dialog.Owner = Window.GetWindow(this); dialog.ShowDialog();
}
return;
Expand Down
1 change: 1 addition & 0 deletions Controls/HistoryWorkspace.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public partial class HistoryWorkspace : UserControl

/// <summary>The history the shown entry belongs to, which picks its baseline. Set by the shell.</summary>
internal Func<HistoryStore> HistorySource { get; set; } = () => ScanHistory.Store;
internal Func<HistoryStore, SurveyStore?> SurveySource { get; set; } = _ => null;

private HistoryStore StoreFor(ScanHistoryEntry? entry) => entry?.Owner ?? HistorySource();
private string Loc(string key) => TryFindResource(key) as string ?? key;
Expand Down
64 changes: 63 additions & 1 deletion Controls/HomeAssistantAdviceWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,22 @@ internal sealed class HomeAssistantAdviceWindow : Window
{ ManufacturerModel = IdentityInput.Text, Protocol = ((ComboBoxItem?)ProtocolInput.SelectedItem)?.Tag as string ?? "",
Goal = ((ComboBoxItem?)GoalInput.SelectedItem)?.Tag as string ?? "Monitor" };

internal HomeAssistantAdviceWindow(NetworkDevice device, Action<string> open, Action<string> copy)
internal HomeAssistantAdviceWindow(NetworkDevice device, Action<string> open, Action<string> copy,
HomeAssistantContext? initialContext = null, Action<HomeAssistantContext>? apply = null, string? observation = null,
SurveyStore? surveyStore = null)
{
_device = HomeAssistantText.Freeze(device);
SurveyEntry? savedEntry = null;
if (apply == null && surveyStore != null)
{
try
{
string identity = SurveyStore.Mac(_device.MacAddress);
savedEntry = identity == "" ? null : surveyStore.Load().SingleOrDefault(entry => entry.LinkedMac == identity);
initialContext ??= savedEntry?.Context;
}
catch (Exception) { observation = SurveyText.L("Error"); }
}
_open = open; _copy = copy; CurrentAdvice = HomeAssistantAdvisor.ForDevice(_device);
Title = HomeAssistantText.L("Title"); Width = 920; Height = 760; MinWidth = 540; MinHeight = 380;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
Expand All @@ -36,13 +49,56 @@ internal HomeAssistantAdviceWindow(NetworkDevice device, Action<string> open, Ac
copyPlan.Click += (_, _) => _copy(HomeAssistantText.Describe(_device, CurrentAdvice, CurrentContext));
var close = Button(Application.Current.TryFindResource("Str_Sys_Close") as string ?? "Close"); close.Click += (_, _) => Close();
buttons.Children.Add(catalogue); buttons.Children.Add(copyPlan); buttons.Children.Add(close);
var save = Button(apply == null ? SurveyText.L("Add") : SurveyText.L("Hints")); save.Tag = "survey-save";
save.Click += (_, _) =>
{
if (apply != null) { apply(CurrentContext); Close(); return; }
try
{
var store = surveyStore ?? throw new InvalidOperationException();
SurveyEntry? target = savedEntry;
string? confirmedIpChoice = null;
if (target == null)
{
string mac = SurveyStore.Mac(_device.MacAddress);
var candidates = store.Load().Where(entry => !entry.Manual && _device.IpAddress.Length > 0 &&
entry.Observation?.Device.IpAddress == _device.IpAddress && (entry.LinkedMac == "" || mac == "")).ToArray();
if (candidates.Length > 0)
{
confirmedIpChoice = SurveyPanel.Choose(this, candidates);
if (confirmedIpChoice == null) return;
target = candidates.SingleOrDefault(entry => entry.Id == confirmedIpChoice);
}
}
bool manual = savedEntry == null && _device.IpAddress.Length == 0 && SurveyStore.Mac(_device.MacAddress).Length == 0;
var draft = (target ?? new SurveyEntry { Name = manual ? "" : SurveyStore.DeviceName(_device), Manual = manual,
Observation = manual ? null : new SurveyObservation { Device = _device, LastSeenUtc = _device.Identification?.ObservationCompletedAtUtc } }) with
{ ManufacturerModel = target?.ManufacturerModel ?? CurrentContext.ManufacturerModel,
Protocol = target?.Protocol ?? CurrentContext.Protocol, Goal = target?.Goal ?? CurrentContext.Goal };
if (savedEntry != null) draft = draft with { ManufacturerModel = CurrentContext.ManufacturerModel, Protocol = CurrentContext.Protocol, Goal = CurrentContext.Goal };
var editor = new SurveyEditorWindow(draft, entry =>
{
if (savedEntry != null) { store.Save(entry); savedEntry = entry; return; }
if (manual) { store.AddManual(entry); savedEntry = entry; return; }
var saved = store.AddObserved(_device, candidates => confirmedIpChoice ?? SurveyPanel.Choose(this, candidates), entry);
if (saved == null) throw new OperationCanceledException();
savedEntry = saved;
}, _open, _copy) { Owner = this };
editor.ShowDialog();
}
catch (Exception) { MessageBox.Show(this, SurveyText.L("Error"), SurveyText.L("Title"), MessageBoxButton.OK, MessageBoxImage.Error); }
};
if (apply != null || surveyStore != null) buttons.Children.Insert(0, save);
DockPanel.SetDock(buttons, Dock.Bottom); panel.Children.Add(buttons);
var header = new StackPanel();
string deviceLabel = (device.IpAddress + " " + device.Hostname).Trim();
header.Children.Add(Text(deviceLabel.Length == 0 ? HomeAssistantText.L("Workflow") : deviceLabel, 18));
if (observation != null) header.Children.Add(Text(observation));
var inputs = new StackPanel();
inputs.Children.Add(Text(HomeAssistantText.L("Identity"))); IdentityInput.SetResourceReference(StyleProperty, "DarkTextBox");
System.Windows.Automation.AutomationProperties.SetName(IdentityInput, HomeAssistantText.L("Identity")); inputs.Children.Add(IdentityInput);
ProtocolInput.SetResourceReference(StyleProperty, "DarkComboBox");
GoalInput.SetResourceReference(StyleProperty, "DarkComboBox");
var fields = new WrapPanel();
fields.Children.Add(Field(HomeAssistantText.L("Protocol"), ProtocolInput));
ProtocolInput.Items.Add(new ComboBoxItem { Content = Application.Current.TryFindResource("Str_Access_Unknown") as string ?? "Unknown", Tag = "" });
Expand All @@ -53,6 +109,12 @@ internal HomeAssistantAdviceWindow(NetworkDevice device, Action<string> open, Ac
foreach (var (id, label) in new[] { ("Monitor", "Monitor"), ("Control", "ControlGoal"), ("Media", "MediaGoal") })
GoalInput.Items.Add(new ComboBoxItem { Content = HomeAssistantText.L(label), Tag = id });
GoalInput.SelectedIndex = 0; inputs.Children.Add(fields);
if (initialContext != null)
{
IdentityInput.Text = initialContext.ManufacturerModel;
ProtocolInput.SelectedItem = ProtocolInput.Items.Cast<ComboBoxItem>().FirstOrDefault(item => item.Tag as string == initialContext.Protocol) ?? ProtocolInput.Items[0];
GoalInput.SelectedItem = GoalInput.Items.Cast<ComboBoxItem>().FirstOrDefault(item => item.Tag as string == initialContext.Goal) ?? GoalInput.Items[0];
}
header.Children.Add(Expand(HomeAssistantText.L("Inputs"), inputs));
DockPanel.SetDock(header, Dock.Top); panel.Children.Add(header);
panel.Children.Add(new ScrollViewer { Content = Results, VerticalScrollBarVisibility = ScrollBarVisibility.Auto, HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled });
Expand Down
50 changes: 45 additions & 5 deletions Controls/HomeAssistantOverviewWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,16 @@ internal sealed class Row(NetworkDevice device)
}
internal IReadOnlyList<Row> Rows { get; }
internal readonly DataGrid Grid = new() { AutoGenerateColumns = false, IsReadOnly = true, CanUserAddRows = false,
SelectionMode = DataGridSelectionMode.Single, HeadersVisibility = DataGridHeadersVisibility.Column,
SelectionMode = DataGridSelectionMode.Extended, HeadersVisibility = DataGridHeadersVisibility.Column,
RowHeaderWidth = 0, MinRowHeight = 54, HorizontalScrollBarVisibility = ScrollBarVisibility.Auto };
private readonly Action<string> _open, _copy;
internal HomeAssistantOverviewWindow(IEnumerable<NetworkDevice> devices, Action<string> open, Action<string> copy)
private readonly SurveyStore? _survey;

internal HomeAssistantOverviewWindow(IEnumerable<NetworkDevice> devices, Action<string> open, Action<string> copy, LocationData? location = null)
{
_open = open; _copy = copy;
_survey = location?.Survey;
SurveyPanel.Theme(this);
Rows = devices.Select(HomeAssistantText.Freeze).OrderBy(device => device.IpSortKey).Select(device => new Row(device)).ToArray();
Title = HomeAssistantText.L("AllTitle"); Width = 1180; Height = 720; MinWidth = 660; MinHeight = 380;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
Expand All @@ -61,7 +65,7 @@ Button AddButton(string label, string tag, Action action)
button.SetResourceReference(StyleProperty, "OutlineButton"); button.Click += (_, _) => action(); buttons.Children.Add(button); return button;
}
var detail = AddButton(HomeAssistantText.L("Details"), "detail", OpenSelected); detail.IsEnabled = false;
AddButton(HomeAssistantText.L("Copy"), "copy-all", () => _copy(Describe()));
var copyAll = AddButton(HomeAssistantText.L("Copy"), "copy-all", () => _copy(Describe()));
AddButton(Application.Current.TryFindResource("Str_Sys_Close") as string ?? "Close", "close", Close);
DockPanel.SetDock(buttons, Dock.Bottom); root.Children.Add(buttons);
void Column(string title, string path, double width)
Expand All @@ -79,10 +83,46 @@ void Column(string title, string path, double width)
Grid.ItemsSource = Rows;
Grid.SelectionChanged += (_, _) => detail.IsEnabled = Grid.SelectedItem is Row;
Grid.MouseDoubleClick += (_, e) => { if (ItemsControl.ContainerFromElement(Grid, e.OriginalSource as DependencyObject) is DataGridRow) OpenSelected(); };
root.Children.Add(Grid); Content = root;
var scan = new DockPanel();
var tabs = new TabControl();
SurveyPanel? survey = null;
if (_survey != null)
{
survey = new SurveyPanel(_survey, Rows.Select(row => row.Device), open, copy);
var add = SurveyPanel.Button(SurveyText.L("Add"), () =>
{
var selected = Grid.SelectedItems.Cast<Row>().Select(row => row.Device).ToArray();
tabs.SelectedIndex = 1;
survey.AddSelected(selected);
}, "survey-add");
add.IsEnabled = false;
Grid.SelectionChanged += (_, _) => add.IsEnabled = Grid.SelectedItems.Count > 0;
DockPanel.SetDock(add, Dock.Bottom);
scan.Children.Add(add);
}
scan.Children.Add(Grid);
tabs.SetResourceReference(Control.BackgroundProperty, "BackgroundBrush");
tabs.SetResourceReference(Control.ForegroundProperty, "TextBrush");
tabs.Items.Add(new TabItem { Header = SurveyText.L("Scan"), Content = scan });
var unavailable = location?.IsTemporary == true
? Application.Current.TryFindResource("Str_Loc_TemporaryNotice") as string ?? SurveyText.L("Error")
: SurveyText.L("Error");
tabs.Items.Add(new TabItem
{
Header = SurveyText.L("Title") + (location?.Info is { } info ? " · " + info.Name : ""),
Content = (FrameworkElement?)survey ?? SurveyPanel.Text(unavailable)
});
tabs.SelectionChanged += (_, args) =>
{
if (!ReferenceEquals(args.Source, tabs)) return;
detail.IsEnabled = tabs.SelectedIndex == 0 && Grid.SelectedItem is Row;
copyAll.IsEnabled = tabs.SelectedIndex == 0;
if (tabs.SelectedIndex == 1) survey?.Reload();
};
root.Children.Add(tabs); Content = root;
}
internal HomeAssistantAdviceWindow? CreateSelected() => Grid.SelectedItem is Row row
? new HomeAssistantAdviceWindow(row.Device, _open, _copy) : null;
? new HomeAssistantAdviceWindow(row.Device, _open, _copy, surveyStore: _survey) : null;
private void OpenSelected()
{
if (CreateSelected() is not { } dialog) return;
Expand Down
4 changes: 2 additions & 2 deletions Controls/ScanWorkspace/HomeAssistantActions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ namespace KillerScan.Controls;
public partial class ScanWorkspace
{
private void HomeAssistantAll_Click(object sender, RoutedEventArgs e) { var window = CreateHomeAssistantOverview(); window.Owner = Window.GetWindow(this); window.ShowDialog(); }
internal HomeAssistantOverviewWindow CreateHomeAssistantOverview() => new(ActiveDevices, WebLink.Open, CopyDeviceText);
internal HomeAssistantAdviceWindow CreateHomeAssistantAdvice(NetworkDevice? device = null) => new(device ?? GetSelectedDevice() ?? new NetworkDevice(), WebLink.Open, CopyDeviceText);
internal HomeAssistantOverviewWindow CreateHomeAssistantOverview() => new(ActiveDevices, WebLink.Open, CopyDeviceText, _location);
internal HomeAssistantAdviceWindow CreateHomeAssistantAdvice(NetworkDevice? device = null) => new(device ?? GetSelectedDevice() ?? new NetworkDevice(), WebLink.Open, CopyDeviceText, surveyStore: _location.Survey);
private void HomeAssistant_Click(object sender, RoutedEventArgs e) { var window = CreateHomeAssistantAdvice(); window.Owner = Window.GetWindow(this); window.ShowDialog(); }
private void HomeAssistantRow_Click(object sender, RoutedEventArgs e)
{ if ((sender as FrameworkElement)?.DataContext is NetworkDevice device) { var window = CreateHomeAssistantAdvice(device); window.Owner = Window.GetWindow(this); window.ShowDialog(); } }
Expand Down
6 changes: 6 additions & 0 deletions Controls/ScanWorkspace/Location.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ private bool SetLocation(LocationData data, bool keepResults)
if (!discard) return false;
}
if (clear) ClearResults();
if (keepResults)
foreach (var device in _active.Devices)
{
device.LocationId = data.Info?.Id;
device.LocationName = data.Info?.Name;
}
EnterLocation(data);
return true;
}
Expand Down
16 changes: 14 additions & 2 deletions Controls/ScanWorkspace/Rescan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ private async void RescanSelected_Click(object sender, RoutedEventArgs e)

var s = _active;
var scanner = s.Scanner;
var location = _location;
var surveyObservations = new List<NetworkDevice>();
_rescanCts = new CancellationTokenSource();
var ct = _rescanCts.Token;
UpdateDeepScanButton();
Expand Down Expand Up @@ -92,7 +94,12 @@ private async void RescanSelected_Click(object sender, RoutedEventArgs e)
catch { done++; ScanProgress.Value = done * 100.0 / targets.Count; continue; }

int idx = s.Devices.IndexOf(old);
if (idx >= 0) { s.Devices[idx] = fresh; refreshed++; }
if (idx >= 0)
{
s.Devices[idx] = fresh;
surveyObservations.Add(fresh);
refreshed++;
}

done++;
ScanProgress.Value = done * 100.0 / targets.Count;
Expand All @@ -105,6 +112,7 @@ private async void RescanSelected_Click(object sender, RoutedEventArgs e)
NoteRefresh(s, refreshed, targets.Count - refreshed, canceled);
_rescanCts.Dispose();
_rescanCts = null;
if (!LeaveDeletedLocation()) RefreshSurvey(location, surveyObservations);
FinishRescanUi();
}
}
Expand Down Expand Up @@ -138,6 +146,8 @@ private async Task DeepScanAllAsync(bool recordProfile = false)

var s = _active;
var scanner = s.Scanner;
var location = _location;
var surveyObservations = new List<NetworkDevice>();
var targets = s.Devices.OrderBy(d => d.IpSortKey).ToList();
// The automatic deep phase belongs to the run that already validated its adapter.
if (!recordProfile)
Expand Down Expand Up @@ -173,6 +183,7 @@ private async Task DeepScanAllAsync(bool recordProfile = false)
if (idx >= 0)
{
s.Devices[idx] = fresh;
surveyObservations.Add(fresh);
refreshed++;
}
}
Expand Down Expand Up @@ -208,10 +219,11 @@ private async Task DeepScanAllAsync(bool recordProfile = false)
{
try
{
bool left = LeaveDeletedLocation();
if (!left) RefreshSurvey(location, surveyObservations);
if (recordProfile)
{
_scanCompleted = completion == ScanCompletion.Complete;
LeaveDeletedLocation();
RecordCompletedRun(options, deep: true, completion);
}
// A manual Deep Scan All refreshes an existing snapshot; an automatic one is
Expand Down
6 changes: 5 additions & 1 deletion Controls/ScanWorkspace/Scanning.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,11 @@ private async void ScanBtn_Click(object sender, RoutedEventArgs e)
if (left) location = _location;
if (!runDeep || _active.Devices.Count == 0) RecordCompletedRun(options, deep: false);
NoteKnownDevices(location);
if (!left) NoteUnsaved(location);
if (!left)
{
RefreshSurvey(location, _active.Devices);
NoteUnsaved(location);
}
}
}
catch (OperationCanceledException)
Expand Down
18 changes: 18 additions & 0 deletions Controls/ScanWorkspace/SurveyActions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Windows;
using KillerScan.Services;

namespace KillerScan.Controls;

public partial class ScanWorkspace
{
private void RefreshSurvey(LocationData location, IEnumerable<NetworkDevice> observed)
{
if (_disposed) return;
try { location.Survey?.Refresh(observed); }
catch (Exception)
{
// A survey write error must not turn a completed network scan into a failed scan.
if (!_disposed) MessageBox.Show(Window.GetWindow(this), SurveyText.L("Error"), SurveyText.L("Title"), MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
Loading