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