Bundles several feature streams that have been iterating on the working tree: - Developer Tools page (Debug-only via DEVELOPER_TOOLS symbol): hosts the identification card, manual KWP write + transaction log, ROM/EEPROM dump card with progress banner and completion message, persisted custom-commands library, persisted EEPROM passwords library. New service primitives: IKwpService.SendRawCustomAsync / ReadEepromAsync / ReadRomEepromAsync. Persistence mirrors the Clients XML pattern in two new files (custom_commands.xml, eeprom_passwords.xml). - Auto-test orchestrator (IAutoTestOrchestrator + AutoTestState): linear K-Line read -> unlock -> bench-on -> test sequence with snackbar UI and progress dialog VM, gated on dashboard alarms. - BIP-STATUS display: BipDisplayViewModel + BipDisplayView, RAM read at 0x0106 via IKwpService.ReadBipStatusAsync; status definitions in BipStatusDefinition. - Tests page redesign: TestSectionCard + PhaseTileView replacing the old TestPlanView/TestRunningView/TestDoneView/TestPreconditionsView/ TestSectionView controls and their VMs. - Pump command sliders: Fluent thick-track style with overhang thumb, click-anywhere-and-drag, mouse-wheel adjustment. - Window startup: app.manifest declares PerMonitorV2 DPI awareness, MainWindow installs a WM_GETMINMAXINFO hook in OnSourceInitialized and maximizes there (after the hook is in place) so the app fits the work area exactly on any display configuration. - Misc: PercentToPixelsConverter, seed_aliases.py one-shot pump-alias importer, tools/Import-BipStatus.ps1, kline_eeprom_spec.md and dump-functions reference docs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
234 lines
8.8 KiB
C#
234 lines
8.8 KiB
C#
using System;
|
|
using System.ComponentModel;
|
|
using System.Runtime.InteropServices;
|
|
using System.Windows;
|
|
using System.Windows.Controls.Primitives;
|
|
using System.Windows.Interop;
|
|
using HC_APTBS.ViewModels;
|
|
using Wpf.Ui.Controls;
|
|
|
|
namespace HC_APTBS;
|
|
|
|
/// <summary>
|
|
/// Code-behind for MainWindow. Bridges <see cref="NavigationView"/> selection
|
|
/// to <see cref="MainViewModel.SelectedPage"/> in both directions.
|
|
///
|
|
/// Click handling: NavigationViewItem inherits from ButtonBase, so we listen
|
|
/// for the bubbling Click routed event at the NavigationView level. This
|
|
/// avoids NavigationView.SelectionChanged + Navigate(), which require items
|
|
/// to set TargetPageType (we don't — page hosting stays with the TabControl).
|
|
///
|
|
/// Programmatic selection: WPF-UI 3.0.5's NavigationView.SelectedItem is a
|
|
/// CLR property with `protected set` — there is no SelectedItemProperty DP
|
|
/// to set. Instead we drive each item's IsActive flag directly by calling
|
|
/// the public NavigationViewItem.Activate / Deactivate methods, which is
|
|
/// exactly what WPF-UI's own navigation pipeline does internally. This is
|
|
/// what makes the IsActive trigger in the BoxyNavItemTemplate fire.
|
|
/// </summary>
|
|
public partial class MainWindow : FluentWindow
|
|
{
|
|
private readonly MainViewModel _viewModel;
|
|
private NavigationViewItem? _activeItem;
|
|
|
|
public MainWindow(MainViewModel viewModel)
|
|
{
|
|
InitializeComponent();
|
|
|
|
// Maximisation is handled in the OnSourceInitialized override below, AFTER
|
|
// the WM_GETMINMAXINFO hook is installed. Setting WindowState=Maximized in
|
|
// XAML applies during InitializeComponent and triggers the first maximize
|
|
// before any hook can run — that's what produces the "bigger than fullscreen
|
|
// and clipped" symptom on FluentWindow / DPI-scaled displays.
|
|
|
|
_viewModel = viewModel;
|
|
DataContext = viewModel;
|
|
|
|
// Catch every NavigationViewItem click that bubbles up.
|
|
NavView.AddHandler(
|
|
ButtonBase.ClickEvent,
|
|
new RoutedEventHandler(OnNavItemClicked));
|
|
|
|
#if DEVELOPER_TOOLS
|
|
InjectDeveloperToolsPage();
|
|
#endif
|
|
|
|
Loaded += OnLoaded;
|
|
viewModel.PropertyChanged += OnViewModelPropertyChanged;
|
|
}
|
|
|
|
#if DEVELOPER_TOOLS
|
|
/// <summary>
|
|
/// Adds the Developer Tools nav item and tab at runtime. Done in code-behind
|
|
/// rather than XAML so the consumer Release build (which removes the page
|
|
/// files entirely) has zero references to a type that doesn't exist there.
|
|
/// The nav item is appended to <c>FooterMenuItems</c> next to Settings; the
|
|
/// hosting <see cref="System.Windows.Controls.TabItem"/> is appended to
|
|
/// <c>PageTabs</c> at index 6, matching <c>AppPage.Developer</c>.
|
|
/// </summary>
|
|
private void InjectDeveloperToolsPage()
|
|
{
|
|
var navItem = new Wpf.Ui.Controls.NavigationViewItem
|
|
{
|
|
Content = "Developer",
|
|
Tag = AppPage.Developer,
|
|
TargetPageTag = "Developer",
|
|
Icon = new Wpf.Ui.Controls.SymbolIcon { Symbol = Wpf.Ui.Controls.SymbolRegular.WrenchScrewdriver24 },
|
|
};
|
|
NavView.FooterMenuItems.Add(navItem);
|
|
|
|
var page = new Views.Pages.DeveloperPage();
|
|
page.SetBinding(System.Windows.FrameworkElement.DataContextProperty,
|
|
new System.Windows.Data.Binding(nameof(MainViewModel.DeveloperPage)));
|
|
PageTabs.Items.Add(new System.Windows.Controls.TabItem { Content = page });
|
|
}
|
|
#endif
|
|
|
|
private void OnLoaded(object sender, RoutedEventArgs e)
|
|
{
|
|
SyncSelectionFromViewModel();
|
|
}
|
|
|
|
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
{
|
|
if (e.PropertyName == nameof(MainViewModel.SelectedPage))
|
|
SyncSelectionFromViewModel();
|
|
}
|
|
|
|
private void OnNavItemClicked(object sender, RoutedEventArgs e)
|
|
{
|
|
if (e.OriginalSource is DependencyObject source
|
|
&& FindAncestor<NavigationViewItem>(source) is { Tag: AppPage page } item
|
|
&& _viewModel.SelectedPage != page)
|
|
{
|
|
_viewModel.SelectedPage = page;
|
|
}
|
|
}
|
|
|
|
private void SyncSelectionFromViewModel()
|
|
{
|
|
var item = FindItemForPage(_viewModel.SelectedPage);
|
|
if (item is null || ReferenceEquals(_activeItem, item))
|
|
return;
|
|
|
|
// Flip IsActive on the previous and new items. NavigationViewItem
|
|
// exposes Activate/Deactivate publicly via INavigationViewItem; both
|
|
// call SetCurrentValue(IsActiveProperty, true/false) internally,
|
|
// which fires the IsActive trigger in BoxyNavItemTemplate.
|
|
_activeItem?.Deactivate(NavView);
|
|
item.Activate(NavView);
|
|
_activeItem = item;
|
|
}
|
|
|
|
private NavigationViewItem? FindItemForPage(AppPage page)
|
|
{
|
|
foreach (var menu in NavView.MenuItems)
|
|
if (menu is NavigationViewItem nvi && nvi.Tag is AppPage tag && tag == page)
|
|
return nvi;
|
|
|
|
foreach (var footer in NavView.FooterMenuItems)
|
|
if (footer is NavigationViewItem nvi && nvi.Tag is AppPage tag && tag == page)
|
|
return nvi;
|
|
|
|
return null;
|
|
}
|
|
|
|
private static T? FindAncestor<T>(DependencyObject? start) where T : DependencyObject
|
|
{
|
|
for (var node = start; node is not null; node = System.Windows.Media.VisualTreeHelper.GetParent(node))
|
|
if (node is T match)
|
|
return match;
|
|
return null;
|
|
}
|
|
|
|
private void OnWindowClosing(object sender, CancelEventArgs e)
|
|
{
|
|
// Allow the window to close; services are disposed by the DI container.
|
|
}
|
|
|
|
// ── Maximised-bounds hook (WM_GETMINMAXINFO) ──────────────────────────────
|
|
|
|
private const int WM_GETMINMAXINFO = 0x0024;
|
|
private const int MONITOR_DEFAULTTONEAREST = 2;
|
|
|
|
/// <summary>
|
|
/// Override (instead of event subscription) so the hook is installed at the
|
|
/// earliest possible point in the window lifecycle, before any subsequent
|
|
/// <c>WM_GETMINMAXINFO</c>. The maximize state itself is set here too, so it
|
|
/// runs only after the hook is in place.
|
|
/// </summary>
|
|
protected override void OnSourceInitialized(EventArgs e)
|
|
{
|
|
base.OnSourceInitialized(e);
|
|
var hwnd = new WindowInteropHelper(this).Handle;
|
|
HwndSource.FromHwnd(hwnd)?.AddHook(WindowProc);
|
|
WindowState = WindowState.Maximized;
|
|
}
|
|
|
|
private static IntPtr WindowProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
|
{
|
|
if (msg == WM_GETMINMAXINFO)
|
|
{
|
|
ApplyWorkAreaMaxBounds(hwnd, lParam);
|
|
handled = true;
|
|
}
|
|
return IntPtr.Zero;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constrains the window's <c>WM_GETMINMAXINFO</c> response to the current monitor's
|
|
/// work area. Without this hook, a maximised <see cref="Window"/> takes the full
|
|
/// monitor rectangle (including the taskbar) and gains an 8 px "phantom border" past
|
|
/// each edge, which is what produces the "bigger than full screen and clipped" symptom.
|
|
/// </summary>
|
|
private static void ApplyWorkAreaMaxBounds(IntPtr hwnd, IntPtr lParam)
|
|
{
|
|
var mmi = Marshal.PtrToStructure<MINMAXINFO>(lParam);
|
|
IntPtr monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
|
|
if (monitor != IntPtr.Zero)
|
|
{
|
|
var info = new MONITORINFO { cbSize = Marshal.SizeOf<MONITORINFO>() };
|
|
if (GetMonitorInfo(monitor, ref info))
|
|
{
|
|
var work = info.rcWork;
|
|
var monitorRect = info.rcMonitor;
|
|
mmi.ptMaxPosition.x = Math.Abs(work.left - monitorRect.left);
|
|
mmi.ptMaxPosition.y = Math.Abs(work.top - monitorRect.top);
|
|
mmi.ptMaxSize.x = Math.Abs(work.right - work.left);
|
|
mmi.ptMaxSize.y = Math.Abs(work.bottom - work.top);
|
|
}
|
|
}
|
|
Marshal.StructureToPtr(mmi, lParam, true);
|
|
}
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern IntPtr MonitorFromWindow(IntPtr hwnd, int dwFlags);
|
|
|
|
[DllImport("user32.dll", CharSet = CharSet.Auto)]
|
|
private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFO lpmi);
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct POINT { public int x; public int y; }
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct RECT { public int left, top, right, bottom; }
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct MONITORINFO
|
|
{
|
|
public int cbSize;
|
|
public RECT rcMonitor;
|
|
public RECT rcWork;
|
|
public uint dwFlags;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct MINMAXINFO
|
|
{
|
|
public POINT ptReserved;
|
|
public POINT ptMaxSize;
|
|
public POINT ptMaxPosition;
|
|
public POINT ptMinTrackSize;
|
|
public POINT ptMaxTrackSize;
|
|
}
|
|
}
|