YellowBox.psm1
# Update-TypeData (Join-Path (ls $MyInvocation.MyCommand.Path).DirectoryName "YellowBox.Types.ps1xml") #region Internal <# .SYNOPSIS If necessary, abbreviates a given text. .PARAMETER Text Text which, if it surpasses a maximum length, gets abbreviated. .PARAMETER MaxLength Length to which, if needed, the given text gets abbreviated. .OUTPUTS Potentially abbreviated text. #> function AbbreviateText([string]$Text, [int]$MaxLength) { if ($Text.Length -le [Math]::Abs($MaxLength)) { return $Text } elseif ($MaxLength -ge 0) # start of string followed by ellipsis { # 0123456 # "abcdefg" # => "abc..." return $Text.Substring(0, $MaxLength - 3) + "..." } else # ellipsis followed by end of string { # 0123456 # "abcdefg" # => "...efg" return "..." + $Text.Substring( $Text.Length + $MaxLength + 3, -($MaxLength + 3)) } } <# .SYNOPSIS Parses a hotkey string specification into a modifier and a key value. .PARAMETER Hotkey Hotkey string specification. .OUTPUTS Pair of 1) a System.UInt16 modifier keys value and 2) a System.Windows.Forms.Keys value. #> function ParseHotkey([string] $Hotkey) { [UInt16] $m = 0 [Windows.Forms.Keys] $k = [Windows.Forms.Keys]::None foreach ($p in $Hotkey.Split("+", ([StringSplitOptions]::RemoveEmptyEntries))) { switch ($p) { "Alt" { $m = $m -bor 0x0001 } "Control" { $m = $m -bor 0x0002 } "Ctrl" { $m = $m -bor 0x0002 } "Shift" { $m = $m -bor 0x0004 } default { $k = [Windows.Forms.Keys][Enum]::Parse([Windows.Forms.Keys], $p) } } } return $m, $k } #region Pattern-related Functions # These functions ought to get exported; finding approved verbs might be a challenge. function Test-UIPattern( [parameter(ValueFromPipeline=$true)] $UIElement, [YellowBox.PatternId] $PatternId) { (Get-UiaPattern -UIElement $UIElement -PatternId $PatternId) -ne $null } function Collapse-UIElement( [parameter(ValueFromPipeline=$true)] $UIElement) { process { foreach ($e in $UIElement) { $expandCollapsePattern = Get-UiaPattern -UIElement $e -PatternId ([YellowBox.PatternId]::ExpandCollapse) if ($expandCollapsePattern -eq $null) { Write-Error "UIElement does not support the 'ExpandCollapse' pattern." continue } $expandCollapsePattern.Collapse() } } } function Expand-UIElement( [parameter(ValueFromPipeline=$true)] $UIElement) { process { foreach($e in $UIElement) { $expandCollapsePattern = Get-UiaPattern -UIElement $e -PatternId ([YellowBox.PatternId]::ExpandCollapse) if ($expandCollapsePattern -eq $null) { Write-Error "UIElement does not support the 'ExpandCollapse' pattern." continue } $expandCollapsePattern.Expand() } } } function Invoke-UIElement( [parameter(ValueFromPipeline=$true)] $UIElement) { process { foreach($e in $UIElement) { $invokePattern = Get-UiaPattern -UIElement $e -PatternId ([YellowBox.PatternId]::Invoke) if ($invokePattern -eq $null) { Write-Error "UIElement does not support the 'Invoke' pattern." continue } $invokePattern.Invoke() } } } function Close-UIElement( [parameter(ValueFromPipeline=$true)] $UIElement) { process { foreach($e in $UIElement) { $windowPattern = Get-UiaPattern -UIElement $e -PatternId ([YellowBox.PatternId]::Window) if ($windowPattern -eq $null) { Write-Error "UIElement does not support the 'Window' pattern." continue } $windowPattern.Close() } } } function Minimize-UIElement([parameter(ValueFromPipeline=$true)] $UIElement) { process { foreach($e in $UIElement) { $windowPattern = Get-UiaPattern -UIElement $e -PatternId ([YellowBox.PatternId]::Window) if ($windowPattern -eq $null) { Write-Error "UIElement does not support the 'Window' pattern." continue } $windowPattern.WindowVisualState = [YellowBox.WindowVisualState]::Minimized } } } function Get-UIElementValue( [parameter(ValueFromPipeline=$true)] $UIElement) { process { foreach($e in $UIElement) { $valuePattern = Get-UiaPattern -UIElement $e -PatternId ([YellowBox.PatternId]::Value) if ($valuePattern -eq $null) { Write-Error "UIElement does not support the 'Value' pattern." continue } $valuePattern.Value } } } #endregion # Pattern-related Functions #region XPath-Related Functions # These functions should probably get exported. function Test-UIXPath( [parameter(Mandatory=$true)][string] $XPath, $UIObject) { if ($UIObject -eq $null) { $UIObject = Get-Item UI: } [YellowBox.Client.UIXPathNavigator] $navigator = New-Object YellowBox.Client.UIXPathNavigator $UIObject return $navigator.Matches($XPath) } function Get-UIXPath([YellowBox.Client.UIElement] $Origin, [YellowBox.Client.UIElement] $Target) { [string] $path = "" $current = $Target while ($current -ne $Origin) { [YellowBox.Client.UIElement] $parent = Get-UiaParentElement $current [string] $pathPart = "" while ($true) # not a loop, just a way to prevent excessive nesting { $controlType = $current.ControlType; if ((Select-UIXPath -UIObject $parent -XPath $controlType) -eq $current) { # control type is sufficient identification $pathPart = $controlType break } [string] $filterExpression = "" $automationId = $current.AutomationId if ($automationId -ne $null -and $automationId -ne "") { $filterExpression += "@AutomationId = '$automationId'" if ((Select-UIXPath -UIObject $parent -XPath "$controlType[$filterExpression]") -eq $current) { $pathPart = "$controlType[$filterExpression']" break } $filterExpression += " and " } $className = $current.ClassName if ($className -ne $null -and $className -ne "") { $filterExpression += "@ClassName = '$className'" if ((Select-UIXPath -UIObject $parent -XPath "$controlType[$filterExpression]") -eq $current) { $pathPart = "$controlType[$filterExpression']" break } $filterExpression += " and " } $name = $current.Name if ($name -ne $null -and $name -ne "") { $filterExpression += "@Name = '$name'" if ((Select-UIXPath -UIObject $parent -XPath "$controlType[$filterExpression]") -eq $current) { $pathPart = "$controlType[$filterExpression']" break } } $allMatches = Select-UIXPath -UIObject $parent -XPath "$controlType[$filterExpression]" [int] $index = 1 for(; $index -le $allMatches.Count; ++$index) { if ($allMatches[$index] -eq $current) { break } } if ($index -gt $allMatches.Count) { throw "unable to identify element via index expression" } $pathPart = "$controlType[$filterExpression][$index]" break } if ([string]::IsNullOrEmpty($path)) { $path = "$pathPart" } else { $path = "$pathPart/$path" } $current = $parent } return $path } function Set-LocationViaUIXPath( [parameter(Mandatory=$true)][string] $XPath, $UIObject) { $matches = Select-UIXPath -UIObject $UIObject -XPath $XPath switch ($matches.Count) { 0 { Write-Error "'$XPath' does not match any UI element" } 1 { Set-Location $matches[0] } default { $format = "{0,6} {1,20} {2,20} {3,20} {4,20}" Write-Host "multiple matches" Write-Host ($format -f "Choice", "RuntimeId", "ControlType", "Name", "ClassName") Write-Host ($format -f "------", "---------", "-----------", "----", "---------") for($i = 0; $i -lt $matches.Count; ++$i) { Write-Host ($format -f $i, (AbbreviateText $matches[$i].RuntimeId -20), (AbbreviateText $matches[$i].ControlType 20), (AbbreviateText $matches[$i].Name 20), (AbbreviateText $matches[$i].ClassName 20)) } $choice = Read-Host "Enter choice" Set-Location $matches[$choice] } } } #endregion XPath-Related Functions #region Event-related Functions function Wait-UIEvent( $UIElement = (Get-Item UI:\), [YellowBox.EventId[]]$EventIds = $null, [YellowBox.TreeScope]$TreeScope = [YellowBox.TreeScope]::Subtree, [int] $Timeout = 10, [scriptblock] $Action) { $uiEventSourceId = "YellowBox.WaitForUIEvent.UIEventSourceId" $uiEventListener = New-Object YellowBox.Client.UIEventListener -ArgumentList $UIElement, $EventIds, $TreeScope Register-ObjectEvent -InputObject $uiEventListener -EventName UIEventOccurred -SourceIdentifier $uiEventSourceId $uiEventListener.StartListening() $Action.Invoke() $e = Wait-Event -SourceIdentifier $uiEventSourceId -Timeout $Timeout $retVal = $null if ($e -ne $null) { #Write-Host "PS: received event, $($e.GetType()), $($e.SourceEventArgs.GetType()), $($e.SourceEventArgs.UIElement.ClassName)" Remove-Event -EventIdentifier $e.EventIdentifier $retVal = $e.SourceEventArgs } $uiEventListener.StopListening() Unregister-Event -SourceIdentifier $uiEventSourceId $uiEventListener.Dispose() return $retVal } function Get-UIEvent( $UIElement = (Get-Item UI:\), [YellowBox.EventId[]]$EventIds = $null, [YellowBox.TreeScope]$TreeScope = [YellowBox.TreeScope]::Subtree) { # The loop below could potentially be broken out of via # [console]::KeyAvailable # or # [console]::ReadKey("NoEcho,IncludeKeyDown") # However, this will only work in powershell.exe (a console application), but not powershell_ise.exe. # Therefore, we use our own HotkeyListener instead. $uiEventSourceId = "YellowBox.GetUIEvent.UIEventSourceId" $uiEventListener = New-Object YellowBox.Client.UIEventListener -ArgumentList $UIElement, $EventIds, $TreeScope Register-ObjectEvent -InputObject $uiEventListener -EventName UIEventOccurred -SourceIdentifier $uiEventSourceId $uiEventListener.StartListening() [UInt16] $cancelKey = Register-Hotkey "Enter" [bool] $cont = $true while($cont) { $event = Wait-Event switch($event.SourceIdentifier) { $uiEventSourceId { Remove-Event -EventIdentifier $event.EventIdentifier echo $event.SourceEventArgs } $Global:HotkeySourceId { Remove-Event -EventIdentifier $event.EventIdentifier $cont = $false } } } Unregister-Hotkey $cancelKey $uiEventListener.StopListening() Unregister-Event -SourceIdentifier $uiEventSourceId $uiEventListener.Dispose() } function Wait-UIPropertyChanged( $UIElement = (Get-Item UI:\), [YellowBox.PropertyId[]] $PropertyIds = $null, [YellowBox.TreeScope]$TreeScope = [YellowBox.TreeScope]::Subtree, [int] $Timeout = 10, [scriptblock] $Action) { $eventSourceId = "YellowBox.Wait-UIPropertyChanged.EventSourceId" $eventListener = New-Object YellowBox.Client.UIPropertyChangedEventListener -ArgumentList $UIElement, $PropertyIds, $TreeScope Register-ObjectEvent -InputObject $eventListener -EventName UIEventOccurred -SourceIdentifier $eventSourceId $eventListener.StartListening() $Action.Invoke() $e = Wait-Event -SourceIdentifier $eventSourceId -Timeout $Timeout $retVal = $null if ($e -ne $null) { #Write-Host "PS: received event, $($e.GetType()), $($e.SourceEventArgs.GetType()), $($e.SourceEventArgs.UIElement.ClassName)" Remove-Event -EventIdentifier $e.EventIdentifier $retVal = $e.SourceEventArgs } $eventListener.StopListening() Unregister-Event -SourceIdentifier $eventSourceId $eventListener.Dispose() return $retVal } function Wait-UIValueChanged( $UIElement = (Get-Item UI:\), [YellowBox.TreeScope]$TreeScope = [YellowBox.TreeScope]::Subtree, [int] $Timeout = 10, [scriptblock] $Action) { $eventArg = Wait-UIPropertyChanged -UIElement $UIElement -PropertyIds ([YellowBox.PropertyId]::ValueValue) -TreeScope $TreeScope -Timeout $Timeout -Action $Action return $eventArg } #endregion Event-related Functions #region Hotkey-related Objects and Functions [YellowBox.HotkeyListener] $Global:HotkeyListener [string] $Global:HotkeySourceId = "YellowBox.HotkeySourceId" $Global:HotkeyActions = @{} <# .SYNOPSIS Executes the action associated with a hotkey. .PARAMETER HotkeyId Hotkey identifier. #> function Invoke-HotkeyAction($HotkeyId) { $Global:HotkeyActions[$HotkeyId].Invoke() } #endregion #region Prompt Functions [YellowBox.Highlight] $promptHighlight = $null [scriptblock] $originalPrompt = $null function Set-HighlightPrompt { $script:originalPrompt = (Get-Command Prompt).ScriptBlock $Function:prompt = { HighlightingPrompt } } function HighlightingPrompt() { $currentLocation = Get-Location if ($currentLocation.Provider.Name -eq "UIA") { if ($script:promptHighlight -eq $null) { $script:promptHighlight = New-Object YellowBox.Highlight ([System.Drawing.Color]::LightGreen) } [YellowBox.Client.UIElement] $currentUIElement = Get-Item $currentLocation $script:promptHighlight.SetBounds($currentUIElement.BoundingRectangle) } return $script:originalPrompt.Invoke() } #endregion Prompt Functions #region Speech-related Functions function Speak-UIElement( [parameter(ValueFromPipeline=$true)] $UIElement, [string]$Format = '$Name $ControlType') { process { foreach($e in $UIElement) { $text = Get-UIElementText $e $Format Speak-Text $text } } } #endregion Speech-related Functions #region Miscellaneous Functions function GetAndHighlight-ChildItem($path) { Get-ChildItem $path | Highlight-AllUIElements -Color ([System.Drawing.Color]::LightCyan) -PassThru } function Label-Position( [parameter(ValueFromPipeline=$true)] $position) { process { foreach($p in $position) { [PSCustomObject]@{Position=new-object System.Drawing.Point $_.X,($_.Y - 20); Text="$($_.X), $($_.Y)"} } } } $YellowBoxMacroPattern = New-Object System.Text.RegularExpressions.Regex "\`$(?<MacroName>\w+)" function Get-UIElementText($UIElement, [string]$Format) { $text = $YellowBoxMacroPattern.Replace($Format, { param($m) switch($m.Groups["MacroName"].Value) { "AutomationId" { $UIElement.AutomationId } "Name" { $UIElement.Name } "ClassName" { $UIElement.ClassName } "ControlType" { $UIElement.ControlType } "LocalizedControlType" { $UIElement.LocalizedControlType } "RuntimeId" { $UIElement.RuntimeId } default { $m.Value } } }) if (Test-UIPattern $UIElement "Value") { $value = (Get-UiaPattern $UIElement "Value").Value if ($value -match "^\((\d+(?:\.\d+)?)\)$") { $value = "-" + $Matches[1]} $text += " with value " + $value } return $text } #endregion Miscellaneous Functions #region Highlight Functions function Highlight-AllUIElements( [parameter(ValueFromPipeline=$true)] $UIElement, [System.Drawing.Color] $Color = ([System.Drawing.Color]::Yellow), [int] $Duration = 3, [switch] $PassThru) { begin { $hls = @() } process { foreach($e in $uiElement) { $hl = New-Object YellowBox.Highlight $Color $hls += $hl $hl.SetBounds($e.BoundingRectangle) if ($PassThru) { $e } } } end { Start-Sleep -Seconds $Duration foreach($hl in $hls) { $hl.Dispose() } } } #endregion Highlight Functions #endregion Internal #region Exported function Get-MousePosition( [bool] $Stream = $true, [System.UInt32] $SamplingFrequency = 10) { if ($Stream) { [UInt16] $cancelKey = Register-Hotkey "Enter" $mousePositionSourceId = "YellowBox.GetMousePosition.MousePositionSourceId" $mousePositionListener = [YellowBox.MousePositionListener]::new($SamplingFrequency) Register-ObjectEvent -InputObject $mousePositionListener -EventName MousePositionChanged -SourceIdentifier $mousePositionSourceId $mousePositionListener.StartListening() try { [bool] $cont = $true while($cont) { $objectEvent = Wait-Event # Write-Host "EV: $($objectEvent.GetType()), $($objectEvent.SourceIdentifier), $($objectEvent.SourceEventArgs.GetType())" switch($objectEvent.SourceIdentifier) { $mousePositionSourceId { Remove-Event -EventIdentifier $objectEvent.EventIdentifier Write-Output $objectEvent.SourceEventArgs.Position break } $Global:HotkeySourceId { if ($objectEvent.SourceEventArgs.Id -eq $cancelKey) { Remove-Event -EventIdentifier $objectEvent.EventIdentifier $cont = $false } break } } } } finally { $mousePositionListener.StopListening() Unregister-Event -SourceIdentifier $mousePositionSourceId $mousePositionListener.Dispose() Unregister-Hotkey $cancelKey } } else { Write-Output ([Windows.Forms.Cursor]::Position) } } <# .SYNOPSIS Continuously gets UI elements according to mouse position and key presses. .DESCRIPTION This command loops until interrupted by the user pressing the Enter key. Each loop iteration can determine a current UI element which - if different from the previous UI element - is written to the pipeline. .PARAMETER StartPath String. Path to the initial UI element. If not specified, but -FromMouse is active, the initial UI element is set to the UI element at the mouse position. If neither -StartPath nor -FromMouse are specified, the initial UI element is specified by the current path. If, in that case, the current path does not specify a UI element, an error occurs. .PARAMETER FromMouse Switch. Indicates that the current UI element is obtained from the mouse position. .PARAMETER AdjustWithKeys Switch. Indicates that the current UI element can be adjusted with key strokes. The following key strokes result in the current UI element being set to the corresponding relative of the previous UI element: Cursor Up: Parent. Cursor Down: First child. Cursor Right: Next sibling. Cursor Left: Previous sibling. .PARAMETER MouseSamplingFrequency System.UInt32. Frequency, in Hertz, with which the mouse position is being checked. Ignored unless -FromMouse is specified. .OUTPUTS YellowBox.Client.UIElement. Current UI elements, written to pipeline. #> function Get-UiaElement( [string] $StartPath = "", [switch] $FromMouse = $true, [switch] $AdjustWithKeys = $true, [System.UInt32] $MouseSamplingFrequency = 10) { [YellowBox.Client.UIElement] $currentUIElement = $null [YellowBox.Client.UIElement] $newUIElement = $null if ($StartPath -eq "" -and !$FromMouse) { $currentUIElement = Get-Item (Get-Location).ProviderPath } elseif ($StartPath -ne "") # StartPath trumps mouse position { $currentUIElement = Get-Item $StartPath } elseif ($FromMouse) { $currentUIElement = Get-UiaElementFromPoint ([Windows.Forms.Cursor]::Position) } if ($currentUIElement -eq $null) { throw "Failed to determine a start UI element." } if ($currentUIElement.GetType().Name -ne "UIElement") { throw "This command only works with a path or a current location representing a UI element." } [UInt16] $cancelKey = Register-Hotkey "Enter" [UInt16] $parentKey = 0 [UInt16] $previousSiblingKey = 0 [UInt16] $nextSiblingKey = 0 [UInt16] $firstChildKey = 0 if ($AdjustWithKeys) { $parentKey = Register-Hotkey "Up" $previousSiblingKey = Register-Hotkey "Left" $nextSiblingKey = Register-Hotkey "Right" $firstChildKey = Register-Hotkey "Down" } $mousePositionSourceId = "YellowBox.GetUIElementFromMousePosition.MousePositionSourceId" [YellowBox.MousePositionListener] $mousePositionListener = $null if ($FromMouse) { $mousePositionListener = [YellowBox.MousePositionListener]::new($MouseSamplingFrequency) Register-ObjectEvent -InputObject $mousePositionListener -EventName MousePositionChanged -SourceIdentifier $mousePositionSourceId $mousePositionListener.StartListening() } [bool] $cont = $true [bool] $firstIteration = $true while ($cont) { # 1. First iteration, current element, but no new element. We want to echo the current element. # 2. We have a new element, but it's the same as the current element. We want to skip echoing the new element (we could assign new to current, # though it would be pointless). # 3. We have a new element, and it's different from the current element. We want to echo the new element and assign it to the current element. # 4. We're on a subsequent (not the first) iteration, but the new element is null (i.e. the operation to obtain a new element resulted in no # element). We want to skip echoing the new element. if ($firstIteration) { Write-Output $currentUIElement } else { if ($newUIElement -ne $null) { if ($newUIElement -ne $currentUIElement) { $currentUIElement = $newUIElement echo $currentUIElement } } } $event = Wait-Event switch ($event.SourceIdentifier) { $mousePositionSourceId { Remove-Event -EventIdentifier $event.EventIdentifier $newUIElement = Get-UiaElementFromPoint $event.SourceEventArgs.Position } $Global:HotkeySourceId { Remove-Event -EventIdentifier $event.EventIdentifier switch($event.SourceEventArgs.Id) { $cancelKey { $cont = $false } $parentKey { $newUIElement = Get-UiaParentElement $currentUIElement } $previousSiblingKey { $newUIElement = Get-UiaPreviousSiblingElement $currentUIElement } $nextSiblingKey { $newUIElement = Get-UiaNextSiblingElement $currentUIElement } $firstChildKey { $newUIElement = Get-UiaFirstChildElement $currentUIElement } } } } $firstIteration = $false } Unregister-Hotkey $cancelKey if ($AdjustWithKeys) { Unregister-Hotkey $parentKey Unregister-Hotkey $previousSiblingKey Unregister-Hotkey $nextSiblingKey Unregister-Hotkey $firstChildKey } if ($FromMouse) { $mousePositionListener.StopListening() Unregister-Event -SourceIdentifier $mousePositionSourceId $mousePositionListener.Dispose() } } function Get-UiaPattern( [parameter(ValueFromPipeline=$true)] $UIElement, [object[]] $PatternId) { begin { if ($null -eq $PatternId) { $PatternId = [System.Enum]::GetValues([YellowBox.PatternId]) } else { $normalizedPatternIds = @() foreach ($patid in $PatternId) { if ($patid -is [YellowBox.PatternId]) { $normalizedPatternIds += $patid } elseif ($patid -is [string]) { $normalizedPatternIds += [System.Enum]::Parse([YellowBox.PatternId], $patid, $true) } else { throw "unexpected pattern identifier type" } } $PatternId = $normalizedPatternIds } } process { if ($null -eq $UIElement) { $e = Get-Item (Get-Location).ProviderPath if ($e.GetType().Name -ne "UIElement") { throw "This command only works with a path or a current location representing a UI element" } $UIElement = @($e) } if ($null -eq $PatternId) { $PatternId = [System.Enum]::GetValues([YellowBox.PatternId]) } foreach ($e in $UIElement) { foreach ($i in $PatternId) { $p = $e.GetPattern($i) if ($null -ne $p) { $p } } } } } function Select-UIXPath( [parameter(Mandatory=$true)][string] $XPath, $UIObject) { if ($null -eq $UIObject) { $UIObject = Get-Item UI: } [YellowBox.Client.UIXPathNavigator] $navigator = [YellowBox.Client.UIXPathNavigator]::new($UIObject) $xpMatches = $navigator.Select($XPath) while ($xpMatches.MoveNext()) { $xpMatches.Current.CurrentElement } } <# .SYNOPSIS Sets the location via keys. #> function Set-UiaLocationViaKeys() { $e = $null Get-UiaElement -FromMouse:$false -AdjustWithKeys | Show-UiaElementHighlight -PassThru | Show-UiaElementScreenTag -PassThru | ForEach-Object { $e = $_ } Set-Location $e } function Set-UiaLocationViaMouseAndKeys() { $e = $null Get-UiaElement -FromMouse -AdjustWithKeys | Show-UiaElementHighlight -PassThru | Show-UiaElementScreenTag -PassThru | ForEach-Object { $e = $_ } Set-Location $e } function Show-UiaElementHighlight( [parameter(ValueFromPipeline=$true)] $UIElement, [switch]$PassThru) { begin { $recordCount = 0 $hl = [YellowBox.Highlight]::new() } process { foreach ($e in $UIElement) { $hl.SetBounds($e.BoundingRectangle) if($PassThru){ $e } ++$recordCount } } end { if ($recordCount -eq 1) { Start-Sleep -Seconds 3 } $hl.Dispose() } } function Show-UiaElementScreenTag( [parameter(ValueFromPipeline=$true)] $UIElement, [string]$Format = '"$Name" $ControlType', [switch]$PassThru) { begin { $recordCount = 0 $st = [YellowBox.ScreenTag]::new() } process { foreach($e in $UIElement) { $br = $e.BoundingRectangle $pos = [System.Drawing.Point]::new($br.X - 6, $br.Y - 23) $text = Get-UIElementText $e $Format $st.SetPositionAndText($pos, $text) if ($PassThru) { $e } ++$recordCount } } end { if ($recordCount -eq 1) { Start-Sleep -Seconds 3 } $st.Dispose() } } function Show-UiaTree($Item, [string] $Format = '$ControlType "$Name"', [int] $Depth = [int]::MaxValue) { function Label($Item) { if ($Item -is [YellowBox.Client.UIElement]) { Get-UIElementText $Item $Format } else { $Item.Name } } # homogenize the 'item' arg if ($Item -eq $null) { $Item = Get-Item . } elseif ($Item -is [string]) { $Item = Get-Item $Item } $stack = [System.Collections.Stack]::new() $stack.Push(@{Item = $Item; IndentFormat = '0'; IsLastChild = $false; Level = 0}) while ($stack.Count -gt 0) { $current = $stack.Pop() if ($current.IndentFormat -eq '0') { $indent = '' $connector = '' $childIndentFormat = '' } else { $indent = $current.IndentFormat.Replace("!", "│ ").Replace(".", " ") $connector = if ($Current.IsLastChild) { '└───' } else { '├───' } $childIndentSuffix = if ($current.IsLastChild) { "." } else { "!" } $childIndentFormat = $current.IndentFormat + $childIndentSuffix } Write-Output "$indent$connector$(Label $current.Item)" if ($current.Level -ge $Depth){ continue } $children = Select-UIXPath -UIObject $current.Item -XPath "*" if ($null -eq $children) { continue } elseif ($children -is [array] -and $children.Length -gt 0) { # ensure the last child gets popped off the stack last [array]::Reverse($children); for([int] $i = 0; $i -lt $children.Length; ++$i) { $stack.Push(@{ Item = $children[$i]; IndentFormat = $childIndentFormat; IsLastChild = ($i -eq 0); Level = $current.Level + 1}) } } elseif ($children -is [YellowBox.Client.UIElement]) { $stack.Push(@{ Item = $children; IndentFormat = $childIndentFormat; IsLastChild = $true; Level = $current.Level + 1}) } else { throw "unexpected return from Select-UIXPath" } } } function Wait-WindowOpened( $UIElement = (Get-Item UI:\), [YellowBox.TreeScope]$TreeScope = [YellowBox.TreeScope]::Subtree, [int] $Timeout = 10, [scriptblock] $Action) { $event = Wait-UIEvent -UIElement $UIElement -EventIds ([YellowBox.EventId]::Window_WindowOpened) -TreeScope $TreeScope -Timeout $Timeout -Action $Action $retVal = $null if ($event -ne $null) { $retVal = $event.UIElement } return $retVal } function Wait-WindowClosed( $UIElement = (Get-Item UI:\), [YellowBox.TreeScope]$TreeScope = [YellowBox.TreeScope]::Subtree, [int] $Timeout = 10, [scriptblock] $Action) { Wait-UIEvent -UIElement $UIElement -EventIds ([YellowBox.EventId]::Window_WindowClosed) -TreeScope $TreeScope -Timeout $Timeout -Action $Action } function Wait-MenuOpened( $UIElement = (Get-Item UI:\), [YellowBox.TreeScope]$TreeScope = [YellowBox.TreeScope]::Subtree, [int] $Timeout = 10, [scriptblock] $Action) { $event = Wait-UIEvent -UIElement $UIElement -EventIds ([YellowBox.EventId]::MenuOpened) -TreeScope $TreeScope -Timeout $Timeout -Action $Action $retVal = $null if ($event -ne $null) { $retVal = $event.UIElement } return $retVal } function Wait-MenuClosed( $UIElement = (Get-Item UI:\), [YellowBox.TreeScope]$TreeScope = [YellowBox.TreeScope]::Subtree, [int] $Timeout = 10, [scriptblock] $Action) { Wait-UIEvent -UIElement $UIElement -EventIds ([YellowBox.EventId]::MenuClosed) -TreeScope $TreeScope -Timeout $Timeout -Action $Action } #region Hotkey Functions <# .SYNOPSIS Registers an action to be executed upon a hotkey press. .PARAMETER Hotkey Hotkey specification of the form "<modkey>{'+'<modkey>}'+'<key>". <modkey> can be "Alt", "Control" (alternatively "Ctrl") and "Shift"; <key> is the string representation of one of the System.Windows.Forms.Keys enumeration. .PARAMETER Action Script block to be executed when the hotkey is pressed. .OUTPUTS System.UInt16. Hotkey identifier. #> function Register-Hotkey([string] $Hotkey, [scriptblock] $Action) { [UInt16] $m, [Windows.Forms.Keys]$k = ParseHotkey $Hotkey if ($Global:HotkeyListener -eq $null) { $Global:HotkeyListener = [YellowBox.HotkeyListener]::new() if ($Action -ne $null) { Register-ObjectEvent -InputObject $Global:HotkeyListener -EventName KeyPressed -Action { param($hkl, $kea) ; Invoke-HotkeyAction $kea.Id } -SourceIdentifier $Global:HotkeySourceId | Out-Null } else { Register-ObjectEvent -InputObject $Global:HotkeyListener -EventName KeyPressed -SourceIdentifier $Global:HotkeySourceId | Out-Null } } else { $Global:HotkeyListener.StopListening() } [uint16] $id = $Global:HotkeyListener.AddHotkey($m, $k) $Global:HotkeyActions[$id] = $Action $Global:HotkeyListener.StartListening() return $id } <# .SYNOPSIS Removes the specified hotkey. .PARAMETER Id Identifier of the hotkey to be removed. The identifier was returned by the corresponding Register-Hotkey call. Hotkey identifiers can also be obtained by listing hotkeys via the Get-Hotkey command. #> function Unregister-Hotkey([uint16]$Id) { if ($Global:HotkeyListener -ne $null) { if ($Global:HotkeyListener.RemoveHotkey($Id)) { if ($Global:HotkeyListener.Hotkeys.Count -eq 0) { Unregister-Event -SourceIdentifier $Global:HotkeySourceId $Global:HotkeyListener.Dispose() $Global:HotkeyListener = $null } } } } <# .SYNOPSIS Retrieves hotkeys previously registered via Register-Hotkey. .PARAMETER Id Identifier of the hotkey to retrieve. If unspecified, all registered hotkeys are being retrieved. .OUTPUTS Registered hotkeys. #> function Get-Hotkey([UInt16] $Id = 0) { if ($Global:HotkeyListener -ne $null) { foreach($h in $Global:HotkeyListener.Hotkeys) { if (($Id -eq 0) -or ($h.Id -eq $Id)) { echo $h } } } } #endregion Hotkey Functions #endregion Exported #region Aliases Set-Alias tree Get-Tree Set-Alias cdk Set-LocationViaKeys Set-Alias cdm Set-LocationViaMouseAndKeys Set-Alias cdx Set-LocationViaUIXPath Set-Alias slx Select-UIXPath Set-Alias dirh GetAndHighlight-ChildItem Set-Alias lsh GetAndHighlight-ChildItem #endregion Aliases |