1b10194634
- Client.UI-003: wire Serilog properly per CLAUDE.md — console sink + rolling daily file sink in Program.Main, Log.CloseAndFlush in finally, per-VM Log.ForContext<> loggers. - Client.UI-004: migrate the cert-store folder picker from the obsolete OpenFolderDialog to StorageProvider.OpenFolderPickerAsync (with TryGetFolderFromPathAsync seed + TryGetLocalPath extraction). - Client.UI-006: surface formerly silent catch blocks via an observable StatusMessage on the Subscriptions / Alarms VMs that bubbles up into the shell's status bar; soft fallbacks log at Information level so hard failures stay distinguishable. - Client.UI-009: docs/Client.UI.md now lists Standard Deviation in the Aggregate row of the Query Options table. - Client.UI-010: removed the unused MinDateTimeProperty / MaxDateTimeProperty styled properties from DateTimeRangePicker. - Client.UI-011: updated the cert-store TextBox watermark from the legacy AppData/LmxOpcUaClient/pki to the canonical AppData/OtOpcUaClient/pki. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
170 lines
5.9 KiB
C#
170 lines
5.9 KiB
C#
using System.ComponentModel;
|
|
using System.Reflection;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Platform.Storage;
|
|
using SkiaSharp;
|
|
using Svg.Skia;
|
|
using ZB.MOM.WW.OtOpcUa.Client.UI.ViewModels;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Client.UI.Views;
|
|
|
|
public partial class MainWindow : Window
|
|
{
|
|
public MainWindow()
|
|
{
|
|
InitializeComponent();
|
|
LoadIcon();
|
|
}
|
|
|
|
private void LoadIcon()
|
|
{
|
|
try
|
|
{
|
|
var assembly = Assembly.GetExecutingAssembly();
|
|
using var stream = assembly.GetManifestResourceStream("ZB.MOM.WW.OtOpcUa.Client.UI.Assets.app-icon.svg");
|
|
if (stream == null) return;
|
|
|
|
using var svg = new SKSvg();
|
|
svg.Load(stream);
|
|
if (svg.Picture == null) return;
|
|
|
|
var size = 64;
|
|
using var bitmap = new SKBitmap(size, size);
|
|
using var canvas = new SKCanvas(bitmap);
|
|
canvas.Clear(SKColors.Transparent);
|
|
|
|
var bounds = svg.Picture.CullRect;
|
|
var scale = Math.Min(size / bounds.Width, size / bounds.Height);
|
|
canvas.Translate((size - bounds.Width * scale) / 2, (size - bounds.Height * scale) / 2);
|
|
canvas.Scale(scale);
|
|
canvas.DrawPicture(svg.Picture);
|
|
|
|
using var image = SKImage.FromBitmap(bitmap);
|
|
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
|
|
using var pngStream = new MemoryStream(data.ToArray());
|
|
Icon = new WindowIcon(pngStream);
|
|
}
|
|
catch
|
|
{
|
|
// Icon loading is best-effort
|
|
}
|
|
}
|
|
|
|
protected override void OnLoaded(RoutedEventArgs e)
|
|
{
|
|
base.OnLoaded(e);
|
|
|
|
var browseTreeView = this.FindControl<BrowseTreeView>("BrowseTreePanel");
|
|
var treeView = browseTreeView?.FindControl<TreeView>("BrowseTree");
|
|
if (treeView != null) treeView.SelectionChanged += OnTreeSelectionChanged;
|
|
|
|
var contextMenu = this.FindControl<ContextMenu>("TreeContextMenu");
|
|
if (contextMenu != null) contextMenu.Opening += OnTreeContextMenuOpening;
|
|
|
|
var subscribeItem = this.FindControl<MenuItem>("SubscribeMenuItem");
|
|
if (subscribeItem != null) subscribeItem.Click += OnSubscribeClicked;
|
|
|
|
var viewHistoryItem = this.FindControl<MenuItem>("ViewHistoryMenuItem");
|
|
if (viewHistoryItem != null) viewHistoryItem.Click += OnViewHistoryClicked;
|
|
|
|
var monitorAlarmsItem = this.FindControl<MenuItem>("MonitorAlarmsMenuItem");
|
|
if (monitorAlarmsItem != null) monitorAlarmsItem.Click += OnMonitorAlarmsClicked;
|
|
|
|
var browseCertPath = this.FindControl<Button>("BrowseCertPathButton");
|
|
if (browseCertPath != null) browseCertPath.Click += OnBrowseCertPathClicked;
|
|
}
|
|
|
|
private void OnTreeSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
|
{
|
|
if (DataContext is not MainWindowViewModel vm || sender is not TreeView treeView) return;
|
|
|
|
vm.SelectedTreeNode = treeView.SelectedItem as TreeNodeViewModel;
|
|
|
|
vm.SelectedTreeNodes.Clear();
|
|
foreach (var item in treeView.SelectedItems)
|
|
if (item is TreeNodeViewModel node)
|
|
vm.SelectedTreeNodes.Add(node);
|
|
}
|
|
|
|
private void OnTreeContextMenuOpening(object? sender, CancelEventArgs e)
|
|
{
|
|
if (DataContext is not MainWindowViewModel vm) return;
|
|
|
|
vm.UpdateHistoryEnabledForSelection();
|
|
|
|
var subscribeItem = this.FindControl<MenuItem>("SubscribeMenuItem");
|
|
var viewHistoryItem = this.FindControl<MenuItem>("ViewHistoryMenuItem");
|
|
var monitorAlarmsItem = this.FindControl<MenuItem>("MonitorAlarmsMenuItem");
|
|
|
|
if (subscribeItem != null)
|
|
subscribeItem.IsEnabled = vm.IsConnected && vm.SelectedTreeNodes.Count > 0;
|
|
if (viewHistoryItem != null)
|
|
viewHistoryItem.IsEnabled = vm.IsHistoryEnabledForSelection;
|
|
if (monitorAlarmsItem != null)
|
|
monitorAlarmsItem.IsEnabled = vm.IsConnected && vm.SelectedTreeNodes.Count > 0;
|
|
}
|
|
|
|
private async void OnSubscribeClicked(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is MainWindowViewModel vm)
|
|
await vm.SubscribeSelectedNodesCommand.ExecuteAsync(null);
|
|
}
|
|
|
|
private void OnViewHistoryClicked(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is MainWindowViewModel vm)
|
|
vm.ViewHistoryForSelectedNodeCommand.Execute(null);
|
|
}
|
|
|
|
private async void OnMonitorAlarmsClicked(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is MainWindowViewModel vm)
|
|
await vm.MonitorAlarmsForSelectedNodeCommand.ExecuteAsync(null);
|
|
}
|
|
|
|
private async void OnBrowseCertPathClicked(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is not MainWindowViewModel vm) return;
|
|
|
|
var topLevel = TopLevel.GetTopLevel(this);
|
|
if (topLevel == null) return;
|
|
|
|
IStorageFolder? startLocation = null;
|
|
if (!string.IsNullOrEmpty(vm.CertificateStorePath))
|
|
{
|
|
try
|
|
{
|
|
startLocation = await topLevel.StorageProvider.TryGetFolderFromPathAsync(vm.CertificateStorePath);
|
|
}
|
|
catch
|
|
{
|
|
// Best-effort: if the existing path can't be resolved (missing/permission), open the dialog without it.
|
|
}
|
|
}
|
|
|
|
var folders = await topLevel.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
|
{
|
|
Title = "Select Certificate Store Folder",
|
|
AllowMultiple = false,
|
|
SuggestedStartLocation = startLocation
|
|
});
|
|
|
|
if (folders.Count == 0) return;
|
|
|
|
var picked = folders[0].TryGetLocalPath();
|
|
if (!string.IsNullOrEmpty(picked))
|
|
vm.CertificateStorePath = picked;
|
|
}
|
|
|
|
protected override void OnClosing(WindowClosingEventArgs e)
|
|
{
|
|
if (DataContext is MainWindowViewModel vm)
|
|
{
|
|
vm.SaveSettings();
|
|
vm.Dispose();
|
|
}
|
|
base.OnClosing(e);
|
|
}
|
|
}
|