diff --git a/Controls/HistoryWorkspace.Actions.cs b/Controls/HistoryWorkspace.Actions.cs index e23c668..4e567d1 100644 --- a/Controls/HistoryWorkspace.Actions.cs +++ b/Controls/HistoryWorkspace.Actions.cs @@ -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; diff --git a/Controls/HistoryWorkspace.xaml.cs b/Controls/HistoryWorkspace.xaml.cs index ef39102..1799909 100644 --- a/Controls/HistoryWorkspace.xaml.cs +++ b/Controls/HistoryWorkspace.xaml.cs @@ -17,6 +17,7 @@ public partial class HistoryWorkspace : UserControl /// The history the shown entry belongs to, which picks its baseline. Set by the shell. internal Func HistorySource { get; set; } = () => ScanHistory.Store; + internal Func SurveySource { get; set; } = _ => null; private HistoryStore StoreFor(ScanHistoryEntry? entry) => entry?.Owner ?? HistorySource(); private string Loc(string key) => TryFindResource(key) as string ?? key; diff --git a/Controls/HomeAssistantAdviceWindow.cs b/Controls/HomeAssistantAdviceWindow.cs index 93f9979..27043a6 100644 --- a/Controls/HomeAssistantAdviceWindow.cs +++ b/Controls/HomeAssistantAdviceWindow.cs @@ -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 open, Action copy) + internal HomeAssistantAdviceWindow(NetworkDevice device, Action open, Action copy, + HomeAssistantContext? initialContext = null, Action? 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; @@ -36,13 +49,56 @@ internal HomeAssistantAdviceWindow(NetworkDevice device, Action 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 = "" }); @@ -53,6 +109,12 @@ internal HomeAssistantAdviceWindow(NetworkDevice device, Action 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().FirstOrDefault(item => item.Tag as string == initialContext.Protocol) ?? ProtocolInput.Items[0]; + GoalInput.SelectedItem = GoalInput.Items.Cast().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 }); diff --git a/Controls/HomeAssistantOverviewWindow.cs b/Controls/HomeAssistantOverviewWindow.cs index 5c211e0..b2a5d59 100644 --- a/Controls/HomeAssistantOverviewWindow.cs +++ b/Controls/HomeAssistantOverviewWindow.cs @@ -31,12 +31,16 @@ internal sealed class Row(NetworkDevice device) } internal IReadOnlyList 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 _open, _copy; - internal HomeAssistantOverviewWindow(IEnumerable devices, Action open, Action copy) + private readonly SurveyStore? _survey; + + internal HomeAssistantOverviewWindow(IEnumerable devices, Action open, Action 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; @@ -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) @@ -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().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; diff --git a/Controls/ScanWorkspace/HomeAssistantActions.cs b/Controls/ScanWorkspace/HomeAssistantActions.cs index 036a2ff..dc682ee 100644 --- a/Controls/ScanWorkspace/HomeAssistantActions.cs +++ b/Controls/ScanWorkspace/HomeAssistantActions.cs @@ -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(); } } diff --git a/Controls/ScanWorkspace/Location.cs b/Controls/ScanWorkspace/Location.cs index 411b71c..acc2dea 100644 --- a/Controls/ScanWorkspace/Location.cs +++ b/Controls/ScanWorkspace/Location.cs @@ -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; } diff --git a/Controls/ScanWorkspace/Rescan.cs b/Controls/ScanWorkspace/Rescan.cs index 04261b8..9747031 100644 --- a/Controls/ScanWorkspace/Rescan.cs +++ b/Controls/ScanWorkspace/Rescan.cs @@ -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(); _rescanCts = new CancellationTokenSource(); var ct = _rescanCts.Token; UpdateDeepScanButton(); @@ -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; @@ -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(); } } @@ -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(); var targets = s.Devices.OrderBy(d => d.IpSortKey).ToList(); // The automatic deep phase belongs to the run that already validated its adapter. if (!recordProfile) @@ -173,6 +183,7 @@ private async Task DeepScanAllAsync(bool recordProfile = false) if (idx >= 0) { s.Devices[idx] = fresh; + surveyObservations.Add(fresh); refreshed++; } } @@ -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 diff --git a/Controls/ScanWorkspace/Scanning.cs b/Controls/ScanWorkspace/Scanning.cs index c936480..a3448b4 100644 --- a/Controls/ScanWorkspace/Scanning.cs +++ b/Controls/ScanWorkspace/Scanning.cs @@ -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) diff --git a/Controls/ScanWorkspace/SurveyActions.cs b/Controls/ScanWorkspace/SurveyActions.cs new file mode 100644 index 0000000..c9e56f6 --- /dev/null +++ b/Controls/ScanWorkspace/SurveyActions.cs @@ -0,0 +1,18 @@ +using System.Windows; +using KillerScan.Services; + +namespace KillerScan.Controls; + +public partial class ScanWorkspace +{ + private void RefreshSurvey(LocationData location, IEnumerable 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); + } + } +} diff --git a/Controls/SurveyEditorWindow.cs b/Controls/SurveyEditorWindow.cs new file mode 100644 index 0000000..fe66574 --- /dev/null +++ b/Controls/SurveyEditorWindow.cs @@ -0,0 +1,74 @@ +using System.Windows; +using System.Windows.Controls; +using KillerScan.Services; + +namespace KillerScan.Controls; + +internal sealed class SurveyEditorWindow : Window +{ + internal readonly Dictionary Inputs = new(); + private SurveyEntry _draft; + private readonly Action _save; + private readonly Action _open, _copy; + internal SurveyEditorWindow(SurveyEntry draft, Action save, Action open, Action copy) + { + _draft = draft; _save = save; _open = open; _copy = copy; + SurveyPanel.Theme(this); + Title = SurveyText.L("Edit"); Width = 700; Height = 820; MinWidth = 420; MinHeight = 380; + WindowStartupLocation = WindowStartupLocation.CenterOwner; + Loaded += (_, _) => KillerFamily.Windowing.WorkAreaFit.Fit(this); + SetResourceReference(BackgroundProperty, "BackgroundBrush"); SetResourceReference(ForegroundProperty, "TextBrush"); SetResourceReference(FontFamilyProperty, "UiFont"); + var root = new DockPanel { Margin = new Thickness(18) }; + var buttons = new WrapPanel { HorizontalAlignment = HorizontalAlignment.Right }; + buttons.Children.Add(SurveyPanel.Button(SurveyText.L("Save"), () => { if (TrySave()) DialogResult = true; }, "save")); + var cancel = SurveyPanel.Button(SurveyText.L("Cancel"), Close, "cancel"); cancel.IsCancel = true; buttons.Children.Add(cancel); + DockPanel.SetDock(buttons, Dock.Bottom); root.Children.Add(buttons); + var fields = new StackPanel(); + fields.Children.Add(SurveyPanel.Text(SurveyText.L("Notice"))); + void Field(string key, string value, string? label = null, bool multiline = false) + { + fields.Children.Add(SurveyPanel.Text(label ?? SurveyText.L(key))); + var input = new TextBox { Text = value, MaxLength = key == "Name" ? 160 : 8000, MinHeight = multiline ? 65 : 30, + AcceptsReturn = multiline, TextWrapping = TextWrapping.Wrap, VerticalScrollBarVisibility = ScrollBarVisibility.Auto }; + input.SetResourceReference(StyleProperty, "DarkTextBox"); + System.Windows.Automation.AutomationProperties.SetName(input, label ?? SurveyText.L(key)); + Inputs[key] = input; fields.Children.Add(input); + } + Field("Name", draft.Name); Field("Room", draft.Room); + Field("ManufacturerModel", draft.ManufacturerModel, HomeAssistantText.L("Identity")); + Field("Firmware", draft.Firmware); Field("Connection", draft.Connection); + Field("Functions", draft.DesiredFunctions, multiline: true); Field("Notes", draft.Notes, multiline: true); + var hints = SurveyPanel.Text(""); + void UpdateHints() => hints.Text = HomeAssistantText.L("Protocol") + ": " + + (HomeAssistantAdvisor.Integration(_draft.Protocol)?.Name ?? Application.Current.TryFindResource("Str_Access_Unknown") as string ?? "Unknown") + " · " + HomeAssistantText.L("Goal") + ": " + + HomeAssistantText.L(_draft.Goal == "Control" ? "ControlGoal" : _draft.Goal == "Media" ? "MediaGoal" : "Monitor"); + fields.Children.Add(hints); UpdateHints(); + fields.Children.Add(SurveyPanel.Button(HomeAssistantText.L("Title"), () => + { + var dialog = CreateAdvice(); + dialog.Owner = this; dialog.ShowDialog(); + UpdateHints(); + }, "advice")); + fields.Children.Add(SurveyPanel.Text(SurveyPanel.ObservationText(draft))); + root.Children.Add(new ScrollViewer { Content = fields, VerticalScrollBarVisibility = ScrollBarVisibility.Auto }); Content = root; + } + internal HomeAssistantAdviceWindow CreateAdvice() + { + _draft = Draft(); + return new HomeAssistantAdviceWindow(SurveyStore.AdviceDevice(_draft), _open, _copy, _draft.Context, context => + { + _draft = _draft with { ManufacturerModel = context.ManufacturerModel, Protocol = context.Protocol, Goal = context.Goal }; + Inputs["ManufacturerModel"].Text = context.ManufacturerModel; + }, SurveyPanel.ObservationText(_draft)); + } + internal SurveyEntry Draft() => _draft with { Name = Inputs["Name"].Text.Trim(), Room = Inputs["Room"].Text, + ManufacturerModel = Inputs["ManufacturerModel"].Text, Firmware = Inputs["Firmware"].Text, + Connection = Inputs["Connection"].Text, DesiredFunctions = Inputs["Functions"].Text, Notes = Inputs["Notes"].Text }; + internal bool TrySave() + { + if (string.IsNullOrWhiteSpace(Inputs["Name"].Text)) { MessageBox.Show(this, SurveyText.L("Required"), Title); return false; } + try { _save(Draft()); return true; } + catch (OperationCanceledException) { return false; } + catch (Exception) { MessageBox.Show(this, SurveyText.L("Error"), Title, MessageBoxButton.OK, MessageBoxImage.Error); return false; } + } +} diff --git a/Controls/SurveyPanel.cs b/Controls/SurveyPanel.cs new file mode 100644 index 0000000..b8b5bc2 --- /dev/null +++ b/Controls/SurveyPanel.cs @@ -0,0 +1,129 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using KillerScan.Services; + +namespace KillerScan.Controls; + +internal sealed class SurveyPanel : DockPanel +{ + internal static void Theme(Window window) => window.Resources.MergedDictionaries.Add(new ResourceDictionary + { Source = new Uri("pack://application:,,,/KillerScan;component/Controls/SurveyStyles.xaml") }); + private readonly SurveyStore _store; + private readonly Action _open, _copy; + private readonly NetworkDevice[] _devices; + internal readonly DataGrid Grid = new() { AutoGenerateColumns = false, IsReadOnly = true, CanUserAddRows = false, + SelectionMode = DataGridSelectionMode.Single, HeadersVisibility = DataGridHeadersVisibility.Column, MinRowHeight = 44 }; + private readonly TextBlock _status = Text(""); + private readonly List