private/Controls/Set-UiWheelRouting.ps1
|
function Set-UiWheelRouting { <# .SYNOPSIS Sets the wheel routing for a control, and whether the page gets it back at the end of the scroll. #> [CmdletBinding()] param( [Parameter(Mandatory)] [System.Windows.FrameworkElement]$Control, [ValidateSet('Page', 'Edge', 'Capture')] [string]$Mode = 'Page' ) # Edge and a Capture dropdown share this handler, which reads the mode off the sender. $handUp = { param($sender, $eventArgs) trap { Write-Debug "Wheel hand up on $($sender.GetType().Name): $_"; continue } if ($eventArgs.Handled) { return } # Once the dropdown is open it scrolls itself, whatever the mode. if ($sender -is [System.Windows.Controls.ComboBox] -and $sender.IsDropDownOpen) { return } # WPF only steps a ComboBox selection when the box holds keybord focus, so Capture moves it here instead. if ($sender -is [System.Windows.Controls.ComboBox] -and !$sender.Resources.Contains('__WheelEdge')) { $eventArgs.Handled = $true $step = if ($eventArgs.Delta -lt 0) { 1 } else { -1 } $next = $sender.SelectedIndex + $step if ($next -ge 0 -and $next -lt $sender.Items.Count) { $sender.SelectedIndex = $next } return } # Breadth first, so the template's own ScrollViewer turns up before any inside an item, and zero children means the template hasn't been applied yet $scrollViewer = $null $queue = [System.Collections.Generic.Queue[System.Windows.DependencyObject]]::new() $queue.Enqueue($sender) while ($queue.Count -gt 0 -and !$scrollViewer) { $node = $queue.Dequeue() $childCount = [System.Windows.Media.VisualTreeHelper]::GetChildrenCount($node) for ($i = 0; $i -lt $childCount; $i++) { $child = [System.Windows.Media.VisualTreeHelper]::GetChild($node, $i) if ($child -is [System.Windows.Controls.ScrollViewer]) { $scrollViewer = $child; break } $queue.Enqueue($child) } } $passUp = $null -eq $scrollViewer if ($scrollViewer) { $atTop = $scrollViewer.VerticalOffset -le 0 $atBottom = $scrollViewer.VerticalOffset -ge ($scrollViewer.ScrollableHeight - 0.5) $passUp = ($eventArgs.Delta -gt 0 -and $atTop) -or ($eventArgs.Delta -lt 0 -and $atBottom) } if (!$passUp) { return } $eventArgs.Handled = $true $newEvent = [System.Windows.Input.MouseWheelEventArgs]::new($eventArgs.MouseDevice, $eventArgs.Timestamp, $eventArgs.Delta) $newEvent.RoutedEvent = [System.Windows.UIElement]::MouseWheelEvent $newEvent.Source = $sender # WPF marks the wheel handled on a ScrollViewer even at its end, so the climb skips any ancestor already there and takes the nearest one that can still move. $target = $null $climb = [System.Windows.Media.VisualTreeHelper]::GetParent($sender) while ($climb) { if ($climb -is [System.Windows.Controls.ScrollViewer]) { $canMove = if ($eventArgs.Delta -lt 0) { $climb.VerticalOffset -lt ($climb.ScrollableHeight - 0.5) } else { $climb.VerticalOffset -gt 0 } if ($canMove) { $target = $climb; break } } $climb = [System.Windows.Media.VisualTreeHelper]::GetParent($climb) } if (!$target) { $target = $sender.Parent -as [System.Windows.UIElement] } if (!$target) { $target = [System.Windows.Media.VisualTreeHelper]::GetParent($sender) -as [System.Windows.UIElement] } if ($target) { $target.RaiseEvent($newEvent) } } # New-UiTab walks up from the hit element and stands down when it finds __WheelCapture. if ($Mode -eq 'Capture') { if ($Control -is [System.Windows.Controls.ComboBox] -and !$Control.Resources.Contains('__WheelCapture')) { $Control.Add_PreviewMouseWheel($handUp) } $Control.Resources['__WheelCapture'] = $true return } # The style function runs again on every theme switch, and once a control keeps the wheel a later Page or Edge call leaves it alone. if ($Control.Resources.Contains('__WheelCapture')) { return } if ($Mode -eq 'Edge') { $Control.Resources['__WheelCapture'] = $true $Control.Resources['__WheelEdge'] = $true $Control.Add_PreviewMouseWheel($handUp) return } if ($Control.Resources.Contains('__WheelPassthrough')) { return } $Control.Resources['__WheelPassthrough'] = $true $Control.Add_PreviewMouseWheel({ param($sender, $eventArgs) trap { Write-Debug "Wheel routing on $($sender.GetType().Name): $_"; continue } if ($eventArgs.Handled) { return } # Capture or Edge applied later can't detach this handler, so it checks the flag and gets out of the way. if ($sender.Resources.Contains('__WheelCapture')) { return } if ($sender -is [System.Windows.Controls.ComboBox] -and $sender.IsDropDownOpen) { return } $eventArgs.Handled = $true $newEvent = [System.Windows.Input.MouseWheelEventArgs]::new($eventArgs.MouseDevice, $eventArgs.Timestamp, $eventArgs.Delta) $newEvent.RoutedEvent = [System.Windows.UIElement]::MouseWheelEvent $newEvent.Source = $sender # Parent reads null for a control that only exists inside a template, so fall back to the visual one. $target = $sender.Parent -as [System.Windows.UIElement] if (!$target) { $target = [System.Windows.Media.VisualTreeHelper]::GetParent($sender) -as [System.Windows.UIElement] } if ($target) { $target.RaiseEvent($newEvent) } }) } |