From wpf-dev-pack
Syncs WPF Keyboard.Modifiers with ScottPlot's internal keyboard state before MouseWheel events. Fixes Ctrl+Wheel zoom requiring control focus in ScottPlot WPF controls.
npx claudepluginhub christian289/dotnet-with-claudecode --plugin wpf-dev-packThis skill uses the workspace's default tool permissions.
ScottPlot manages its own internal keyboard state via KeyDown/KeyUp events. Since these events require keyboard focus, modifier-dependent MouseWheel interactions (e.g., Ctrl+Wheel zoom) fail until the user clicks the control.
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
ScottPlot manages its own internal keyboard state via KeyDown/KeyUp events. Since these events require keyboard focus, modifier-dependent MouseWheel interactions (e.g., Ctrl+Wheel zoom) fail until the user clicks the control.
WPF routes mouse events and keyboard events differently:
| Event | Routing Basis | Requires Keyboard Focus |
|---|---|---|
MouseWheel | Mouse position (hit-test) | No |
MouseMove, MouseDown | Mouse position (hit-test) | No |
KeyDown, KeyUp | Keyboard focus | Yes |
ScottPlot tracks modifier keys through KeyDown/KeyUp into its internal KeyboardState. Without keyboard focus, KeyDown never fires, so the internal state never records Ctrl as pressed. The MouseWheel event arrives, but the zoom handler checks keys.IsPressed(Control) against the empty internal state and does nothing.
System.Windows.Input.Keyboard.Modifiers reads the OS-level key state globally, regardless of which element has keyboard focus.
Inject the current modifier state into the control's input processor before processing each MouseWheel event:
using System.Windows;
using System.Windows.Input;
public static void ProcessMouseWheel(
this UserInputProcessor processor,
FrameworkElement fe,
MouseWheelEventArgs e)
{
// Sync modifier keys from WPF global state before processing
SyncModifierKeys(processor);
Pixel pixel = e.ToPixel(fe);
IUserAction action = e.Delta > 0
? new MouseWheelUp(pixel)
: new MouseWheelDown(pixel);
processor.Process(action);
}
private static void SyncModifierKeys(UserInputProcessor processor)
{
IUserAction action = (Keyboard.Modifiers & ModifierKeys.Control) != 0
? new KeyDown(StandardKeys.Control)
: new KeyUp(StandardKeys.Control);
processor.Process(action);
}
Keyboard.Modifiers is focus-independent (OS-level global query)KeyDown/KeyUp to the input processor to update its internal keyboard stateAn alternative approach is to call Keyboard.Focus(this) on MouseEnter:
// Alternative approach - has side effects
protected override void OnMouseEnter(MouseEventArgs e)
{
Keyboard.Focus(this);
base.OnMouseEnter(e);
}
| Approach | Pros | Cons |
|---|---|---|
| Sync Keyboard.Modifiers | No side effects, no focus stealing | Must be called on each wheel event |
| Focus on MouseEnter | Simple | Steals focus from TextBox/other inputs, disrupts Tab navigation |
Prefer Keyboard.Modifiers sync - it solves the problem without affecting focus behavior.
// Wrong: sync once and cache
private static bool _synced = false;
private static void SyncModifierKeys(UserInputProcessor processor)
{
if (_synced) return; // Ctrl state can change between wheel events!
_synced = true;
// ...
}
Modifier state must be synced every time before processing MouseWheel, because the user may press or release Ctrl between wheel events.
// Wrong: sync after processing
processor.Process(action);
SyncModifierKeys(processor); // Too late - wheel already processed with stale state
The sync must happen before processor.Process(wheelAction).
UserInputProcessor)Keyboard.Modifiers sync in the MouseWheel processing path