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>
141 lines
6.0 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|