feat: developer tools page, auto-test orchestrator, BIP display, tests redesign
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>
This commit is contained in:
@@ -1,24 +1,233 @@
|
||||
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 — minimal: sets DataContext and forwards the
|
||||
/// Closing event to a ViewModel command if needed.
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user