Files
HC_APTBS/Views/UserControls/PumpCommandsCard.xaml.cs
LucianoDev 827b811b39 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>
2026-05-07 13:59:50 +02:00

141 lines
6.0 KiB
C#

using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
namespace HC_APTBS.Views.UserControls
{
/// <summary>
/// Pump command sliders. Code-behind only carries the click-anywhere-and-drag
/// gesture for the custom <c>FluentThickVerticalSlider</c> template — clicking
/// outside the thumb captures the mouse on the Slider and tracks the value
/// against the Track's geometry until release.
/// </summary>
public partial class PumpCommandsCard : UserControl
{
public PumpCommandsCard() => InitializeComponent();
/// <summary>
/// Wires the click-anywhere-and-drag handlers using
/// <c>handledEventsToo: true</c> so they fire even if a class-level
/// handler (e.g. <c>Slider.OnPreviewMouseLeftButtonDown</c> when
/// <c>IsMoveToPointEnabled</c> is on) marks the event handled.
/// </summary>
private void Slider_Loaded(object sender, RoutedEventArgs e)
{
if (sender is not Slider slider) return;
slider.AddHandler(PreviewMouseLeftButtonDownEvent,
new MouseButtonEventHandler(Slider_PreviewMouseLeftButtonDown), true);
slider.AddHandler(PreviewMouseLeftButtonUpEvent,
new MouseButtonEventHandler(Slider_PreviewMouseLeftButtonUp), true);
slider.AddHandler(MouseMoveEvent,
new MouseEventHandler(Slider_MouseMove), true);
slider.AddHandler(PreviewMouseWheelEvent,
new MouseWheelEventHandler(Slider_PreviewMouseWheel), true);
}
/// <summary>
/// Adjusts the Slider's value by one step per wheel notch while the cursor
/// is over the slider. The step is taken from <see cref="Slider.TickFrequency"/>
/// when available, falling back to <see cref="RangeBase.SmallChange"/>, then
/// 1% of the slider's range. The event is marked handled so the wheel doesn't
/// also scroll a parent <c>ScrollViewer</c>.
/// </summary>
private void Slider_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
if (sender is not Slider slider) return;
double step = slider.TickFrequency;
if (step <= 0) step = slider.SmallChange;
if (step <= 0) step = (slider.Maximum - slider.Minimum) * 0.01;
if (step <= 0) return;
double notches = e.Delta / 120.0;
double newValue = slider.Value + notches * step;
if (newValue < slider.Minimum) newValue = slider.Minimum;
else if (newValue > slider.Maximum) newValue = slider.Maximum;
slider.Value = newValue;
e.Handled = true;
}
/// <summary>
/// On press outside the Thumb, captures the mouse on the Slider so the
/// user can drag from any point on the track in one motion.
/// </summary>
private void Slider_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (sender is not Slider slider) return;
if (slider.Template?.FindName("PART_Track", slider) is not Track track) return;
// Direct thumb press — let the Thumb's own drag handle it.
if (IsClickInsideThumb(e.OriginalSource as DependencyObject, track.Thumb)) return;
UpdateValueFromPoint(slider, e.GetPosition(slider));
slider.CaptureMouse();
e.Handled = true;
}
/// <summary>
/// While the Slider has mouse capture, continuously map the cursor
/// position back to a Slider value.
/// </summary>
private void Slider_MouseMove(object sender, MouseEventArgs e)
{
if (sender is not Slider slider || !slider.IsMouseCaptured) return;
UpdateValueFromPoint(slider, e.GetPosition(slider));
}
/// <summary>
/// Releases the Slider's mouse capture on button-up.
/// </summary>
private void Slider_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (sender is Slider slider && slider.IsMouseCaptured)
{
slider.ReleaseMouseCapture();
e.Handled = true;
}
}
/// <summary>
/// Maps a cursor position (in Slider coordinates) to a Slider value using
/// the Slider's own bounds. Bypasses <c>Track.ValueFromPoint</c>, which
/// can return scaled values for custom templates whose RepeatButton sizes
/// don't match the Track's expected layout.
/// </summary>
private static void UpdateValueFromPoint(Slider slider, Point pointInSlider)
{
bool vertical = slider.Orientation == Orientation.Vertical;
double length = vertical ? slider.ActualHeight : slider.ActualWidth;
if (length <= 0) return;
double pos = vertical ? pointInSlider.Y : pointInSlider.X;
double fraction = pos / length;
if (fraction < 0) fraction = 0;
else if (fraction > 1) fraction = 1;
// Vertical (default): top = Maximum. Horizontal (default): left = Minimum.
// IsDirectionReversed flips that on each axis.
bool flip = vertical ? !slider.IsDirectionReversed : slider.IsDirectionReversed;
if (flip) fraction = 1.0 - fraction;
double value = slider.Minimum + fraction * (slider.Maximum - slider.Minimum);
if (value < slider.Minimum) value = slider.Minimum;
else if (value > slider.Maximum) value = slider.Maximum;
slider.Value = value;
}
private static bool IsClickInsideThumb(DependencyObject? src, Thumb? thumb)
{
if (thumb is null || src is null) return false;
for (var node = src; node is not null; node = VisualTreeHelper.GetParent(node))
{
if (node == thumb) return true;
}
return false;
}
}
}