From 7ddff56b0a38a6df640ab8638fb45354912a3920 Mon Sep 17 00:00:00 2001 From: Grant Kilber Date: Sat, 7 Feb 2026 14:45:08 -0600 Subject: [PATCH 01/20] Fix edge cases with grid snapping. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Guarded against destroyed guide GUIs so CornerRadius won’t throw “Gui has no window” if a guide gets destroyed between creation and sizing. --- README.md | 5 +- config/config.example.toml | 4 ++ docs/COMPILE.md | 22 +++++++++ harken.ahk | 18 ++++++-- src/lib/config_loader.ahk | 2 + src/lib/virtual_desktop.ahk | 91 +++++++++++++++++++++++++++++++++++++ src/lib/window_manager.ahk | 4 ++ 7 files changed, 140 insertions(+), 6 deletions(-) create mode 100644 docs/COMPILE.md diff --git a/README.md b/README.md index 896ddf4..7c2d583 100644 --- a/README.md +++ b/README.md @@ -109,8 +109,8 @@ Enter Command Mode with `super + ;`. | --- | --- | | `super + /` | Show command overlay (temporary) | | `super + w` | Window selector (window walker) | -| `super + c` | Cycle app windows on current desktop | -| `super + shift + c` | Cycle app windows across desktops (not working) | +| `super + c` | Cycle app windows across desktops | +| `super + shift + c` | Cycle app windows on current desktop | | `super + space` | Center width cycle | | `super + m` | Maximize/un-maximize | | `super + q` | Close window | @@ -142,6 +142,7 @@ Enter Command Mode with `super + ;`. | `super + alt + shift + h/l` | Move window to previous/next desktop (follow) | | `super + alt + ` | Go to mapped desktop (`[[virtual_desktop.]]`) | | `super + alt + shift + ` | Move window to mapped desktop (follow) | +| `virtual_desktop.auto_assign` | Move newly created windows that match `apps[]` with `desktop` set | #### Apps (defaults) diff --git a/config/config.example.toml b/config/config.example.toml index 295caeb..5869f3d 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -78,6 +78,7 @@ super_double_tap_ms = 300 cycle_app_windows_hotkey = "c" # Cycle app windows on the current desktop only. cycle_app_windows_current_hotkey = "+c" + # Swap the two values if you prefer current-desktop cycling on `super+c`. center_width_cycle_hotkey = "Space" [window.move_mode] @@ -121,6 +122,9 @@ debug_cycle = false debug_hotkeys = false tray_indicator = true tray_format = "{current}/{total}" +# auto_assign = false +# auto_assign_interval_ms = 500 +# auto_assign applies only to apps with `desktop` set. [[virtual_desktop.1]] hotkey = "1" diff --git a/docs/COMPILE.md b/docs/COMPILE.md new file mode 100644 index 0000000..25014ef --- /dev/null +++ b/docs/COMPILE.md @@ -0,0 +1,22 @@ +## Option 1: Use Ahk2Exe (official GUI) + +1. Install AutoHotkey v2. +2. Open **Ahk2Exe** (included with the AutoHotkey install). +3. Configure: + - **Source (Script)**: your `.ahk` file + - **Destination (Exe)**: output path + - **Base File**: v2 base (e.g. `AutoHotkey64.exe`) + - *(Optional)* **Icon**: `.ico` +4. Click **Convert**. + +Result: a standalone `.exe` that runs without AutoHotkey installed. + + +## Option 2: Command-line Ahk2Exe + +```powershell +& Ahk2Exe.exe ` + /in .\harken.ahk ` + /out .\dist\harken.exe ` + /base .\ahk\AutoHotkey64.exe +``` diff --git a/harken.ahk b/harken.ahk index 44f5642..39543e6 100644 --- a/harken.ahk +++ b/harken.ahk @@ -138,7 +138,9 @@ DefaultConfig() { "debug_cycle", false, "debug_hotkeys", false, "tray_indicator", false, - "tray_format", "{current}/{total}" + "tray_format", "{current}/{total}", + "auto_assign", false, + "auto_assign_interval_ms", 500 ), "directional_focus", Map( "enabled", true, @@ -434,9 +436,17 @@ MatchAppWindow(app, hwnd := 0) { } MatchWindowFields(match, hwnd) { - exe_name := WinGetProcessName("ahk_id " hwnd) - class_name := WinGetClass("ahk_id " hwnd) - title := WinGetTitle("ahk_id " hwnd) + if !WinExist("ahk_id " hwnd) + return false + try exe_name := WinGetProcessName("ahk_id " hwnd) + catch + return false + try class_name := WinGetClass("ahk_id " hwnd) + catch + return false + try title := WinGetTitle("ahk_id " hwnd) + catch + return false if !MatchField(match, "exe", exe_name) return false diff --git a/src/lib/config_loader.ahk b/src/lib/config_loader.ahk index d1e3e0c..97abc10 100644 --- a/src/lib/config_loader.ahk +++ b/src/lib/config_loader.ahk @@ -123,6 +123,8 @@ ConfigSchema() { "debug_hotkeys", "bool", "tray_indicator", "bool", "tray_format", "string", + "auto_assign", "bool", + "auto_assign_interval_ms", "number", "desktop_hotkeys_duplicates", OptionalSpec([Map( "hotkey", "string", "desktop", "number", diff --git a/src/lib/virtual_desktop.ahk b/src/lib/virtual_desktop.ahk index 636e04f..8ed2d2b 100644 --- a/src/lib/virtual_desktop.ahk +++ b/src/lib/virtual_desktop.ahk @@ -9,6 +9,8 @@ VirtualDesktopEnabled() { return Config["virtual_desktop"]["enabled"] } +global vd_auto_assign_timer := 0 + VirtualDesktopTrayEnabled() { global Config if !VirtualDesktopEnabled() @@ -30,6 +32,95 @@ InitVirtualDesktop() { if (ensure_count > 0) VD.createUntil(ensure_count) InitVirtualDesktopTrayIndicator() + InitVirtualDesktopAutoAssign() +} + +InitVirtualDesktopAutoAssign() { + if !VirtualDesktopEnabled() + return + if !Config["virtual_desktop"].Has("auto_assign") || !Config["virtual_desktop"]["auto_assign"] + return + interval := 500 + if Config["virtual_desktop"].Has("auto_assign_interval_ms") + interval := Config["virtual_desktop"]["auto_assign_interval_ms"] + StartVirtualDesktopAutoAssign(interval) +} + +StartVirtualDesktopAutoAssign(interval_ms) { + global vd_auto_assign_timer + if vd_auto_assign_timer + SetTimer(vd_auto_assign_timer, 0) + vd_auto_assign_timer := VirtualDesktopAutoAssignTick + SetTimer(vd_auto_assign_timer, interval_ms) +} + +VirtualDesktopAutoAssignTick(*) { + static seen_hwnds := Map() + if !VirtualDesktopEnabled() + return + if !Config.Has("virtual_desktop") || !Config["virtual_desktop"].Has("auto_assign") || !Config["virtual_desktop"]["auto_assign"] + return + + bak_detect_hidden_windows := A_DetectHiddenWindows + A_DetectHiddenWindows := true + win_list := WinGetList() + A_DetectHiddenWindows := bak_detect_hidden_windows + + for _, hwnd in win_list { + if seen_hwnds.Has(hwnd) + continue + seen_hwnds[hwnd] := true + TryAutoAssignWindow(hwnd) + } + + for hwnd, _ in seen_hwnds { + if !WindowExistsAcrossDesktops(hwnd) + seen_hwnds.Delete(hwnd) + } +} + +TryAutoAssignWindow(hwnd) { + if !WindowExistsAcrossDesktops(hwnd) + return + + for _, app in Config["apps"] { + if !(app is Map) + continue + if !app.Has("desktop") + continue + if !AppConfigMatchesWindow(app, hwnd) + continue + target_desktop := app["desktop"] + if (target_desktop <= 0) + return + total := VD.getCount() + if (target_desktop > total) { + VD.createUntil(target_desktop) + VD.IVirtualDesktopListChanged() + total := VD.getCount() + } + if (target_desktop > total) + return + follow_on_spawn := true + if app.Has("follow_on_spawn") + follow_on_spawn := app["follow_on_spawn"] + VD.MoveWindowToDesktopNum("ahk_id " hwnd, target_desktop, follow_on_spawn) + if follow_on_spawn { + VD.goToDesktopNum(target_desktop) + VD.WaitDesktopSwitched(target_desktop) + } + return + } +} + +AppConfigMatchesWindow(app, hwnd) { + if app.Has("match") && (app["match"] is Map) + return MatchWindowFields(app["match"], hwnd) + + if app.Has("win_title") && app["win_title"] != "" { + try return WinExist(app["win_title"] " ahk_id " hwnd) + } + return false } InitVirtualDesktopTrayIndicator() { diff --git a/src/lib/window_manager.ahk b/src/lib/window_manager.ahk index 39ca5f1..6cb41b7 100644 --- a/src/lib/window_manager.ahk +++ b/src/lib/window_manager.ahk @@ -575,6 +575,10 @@ class Gui_Guides CornerRadius(curve := 15) { + if !this.gui + return + if !WinExist("ahk_id " this.gui.Hwnd) + return this.gui.GetPos(,, &width, &height) ; get position of gui WinSetRegion('0-0 w' width ' h' height ' r' ; use position to round the corners curve '-' curve, this.gui) ; using this curve value From ec92c6e7818d6593c6d3507bca9b7348c728995f Mon Sep 17 00:00:00 2001 From: Grant Kilber Date: Sat, 7 Feb 2026 14:56:36 -0600 Subject: [PATCH 02/20] feat: ignore_classes in app config --- config/config.example.toml | 1 + src/lib/config_loader.ahk | 1 + src/lib/virtual_desktop.ahk | 22 ++++++++++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/config/config.example.toml b/config/config.example.toml index 5869f3d..6ba86ab 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -47,6 +47,7 @@ win_title = "ahk_exe telegram.exe" run = "telegram" run_paths = [] # desktop = 2 +# ignore_classes = ["Qt51518QWindowToolSaveBits"] [[apps]] id = "discord" diff --git a/src/lib/config_loader.ahk b/src/lib/config_loader.ahk index 97abc10..208df5f 100644 --- a/src/lib/config_loader.ahk +++ b/src/lib/config_loader.ahk @@ -49,6 +49,7 @@ ConfigSchema() { "run_paths", OptionalSpec(["string"]), "desktop", OptionalSpec("number"), "follow_on_spawn", OptionalSpec("bool"), + "ignore_classes", OptionalSpec(["string"]), "focus_border", OptionalSpec(Map( "border_color", OptionalSpec("string"), "move_mode_color", OptionalSpec("string"), diff --git a/src/lib/virtual_desktop.ahk b/src/lib/virtual_desktop.ahk index 8ed2d2b..29106c0 100644 --- a/src/lib/virtual_desktop.ahk +++ b/src/lib/virtual_desktop.ahk @@ -82,6 +82,11 @@ VirtualDesktopAutoAssignTick(*) { TryAutoAssignWindow(hwnd) { if !WindowExistsAcrossDesktops(hwnd) return + try ex_style := WinGetExStyle("ahk_id " hwnd) + catch + return + if (ex_style & 0x80) || (ex_style & 0x08000000) + return for _, app in Config["apps"] { if !(app is Map) @@ -90,6 +95,8 @@ TryAutoAssignWindow(hwnd) { continue if !AppConfigMatchesWindow(app, hwnd) continue + if AppConfigIgnoresWindow(app, hwnd) + continue target_desktop := app["desktop"] if (target_desktop <= 0) return @@ -123,6 +130,21 @@ AppConfigMatchesWindow(app, hwnd) { return false } +AppConfigIgnoresWindow(app, hwnd) { + if !(app is Map) + return false + if !app.Has("ignore_classes") || !(app["ignore_classes"] is Array) + return false + try class_name := WinGetClass("ahk_id " hwnd) + catch + return false + for _, ignore_class in app["ignore_classes"] { + if (StrLower(ignore_class) = StrLower(class_name)) + return true + } + return false +} + InitVirtualDesktopTrayIndicator() { if !VirtualDesktopTrayEnabled() return From 813f699e7584b3b704c2b606abea0f9d688fad82 Mon Sep 17 00:00:00 2001 From: Grant Kilber Date: Sat, 7 Feb 2026 15:13:21 -0600 Subject: [PATCH 03/20] Simplify some config by removing `reload` and using `config_watch` --- AGENTS.md | 2 +- README.md | 12 ++++++++- config/config.example.toml | 13 +++------- harken.ahk | 53 +++++++++++++++----------------------- src/hotkeys/unbound.ahk | 4 --- src/hotkeys/window.ahk | 4 +-- src/lib/command_toast.ahk | 2 +- src/lib/config_loader.ahk | 11 +++----- 8 files changed, 42 insertions(+), 59 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8e50ef7..4ecc7d8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -108,7 +108,7 @@ Single test / focused run: ## Suggested Manual Checks - Launch `harken.ahk` with a clean `harken.toml` and verify hotkeys. -- Validate reload flow (normal hotkey and command mode). +- Validate reload flow (command mode). - Confirm Command Overlay and helper tools still open and update. - If touching config schema, ensure errors log correctly in `%APPDATA%\harken\config.errors.log`. - If touching tray indicator: confirm tray icon updates on desktop change. diff --git a/README.md b/README.md index 7c2d583..b75a6f2 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ Enter Command Mode with `super + ;`. - Start the program and enter command mode with `super + ;`. The binary is not currently signed and you will be warned by Windows. Clone and use `harken.ahk` directly as an alternative. - The program might fail on first run? Probably something to do with the config. For now you can create the config first to _maybe_ avoid the initial-crash scenario. - Press `e` to open the config file. You can also find it manually in `~/.config/harken/harken.toml` as it will be created on first run. -- After making changes to your config you can reload the config (the entire program, actually) with `r` while in command mode. +- After making changes to your config you can reload (the entire program) with `r` while in command mode. ### All default keybindings @@ -174,6 +174,16 @@ These are examples for the launch-or-focus keybindings. - In Command Mode, press `i` to launch the window inspector. - Use Refresh to update the list; Copy Selected/All or Export to save results. +### Config watcher + +Optional config file watcher that reloads the program when your config changes. + +```toml +[config_watch] +enabled = false +interval_ms = 2500 +``` + ## Limitations - This has not been tested with multi-monitor setups or much outside of ultra-wide monitors. - Virtual desktop integration requires AutoHotkey v2.1-alpha-18 or later. diff --git a/config/config.example.toml b/config/config.example.toml index 6ba86ab..64a98d9 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -158,12 +158,7 @@ update_interval_ms = 20 enabled = true overlay_opacity = 200 -[reload] -enabled = true -hotkey = "r" -super_key_required = true -watch_enabled = false -watch_interval_ms = 2500 -mode_enabled = true -mode_hotkey = ";" -mode_timeout_ms = 20000 +[config_watch] +enabled = false +# Only used if WinAPI method has to fall back to polling. +interval_ms = 2500 diff --git a/harken.ahk b/harken.ahk index 39543e6..915eca8 100644 --- a/harken.ahk +++ b/harken.ahk @@ -34,29 +34,20 @@ RegisterSuperKeyHotkey("~", "", (*) => OnSuperKeyDown()) if HasSuperKey("CapsLock") SetCapsLockState "AlwaysOff" -reload_config := Config["reload"] -if reload_config["enabled"] { - reload_hotkey := reload_config["hotkey"] - reload_mode_hotkey := reload_config["mode_hotkey"] - reload_mode_enabled := reload_config["mode_enabled"] - reload_mode_timeout := reload_config["mode_timeout_ms"] - global reload_mode_active := false - - if reload_mode_enabled { - RegisterSuperComboHotkey(reload_mode_hotkey, (*) => ActivateReloadMode(reload_mode_timeout)) - HotIf ReloadModeActive - Hotkey(reload_hotkey, (*) => ExecuteCommand(() => Reload())) - Hotkey("i", (*) => ExecuteCommand(OpenWindowInspector)) - Hotkey("n", (*) => ExecuteCommand(ToggleCommandHelper)) - Hotkey("w", (*) => ExecuteCommand(OpenNewWindowForActiveApp)) - Hotkey("e", (*) => ExecuteCommand(OpenConfigFile)) - Hotkey("Esc", ClearReloadMode) - HotIf - } -} - -if reload_config["enabled"] && reload_config["watch_enabled"] - StartConfigWatcher(config_path, reload_config["watch_interval_ms"]) +global reload_mode_active := false +reload_mode_timeout := 20000 +RegisterSuperComboHotkey(";", (*) => ActivateReloadMode(reload_mode_timeout)) +HotIf ReloadModeActive +Hotkey("r", (*) => ExecuteCommand(() => Reload())) +Hotkey("i", (*) => ExecuteCommand(OpenWindowInspector)) +Hotkey("n", (*) => ExecuteCommand(ToggleCommandHelper)) +Hotkey("w", (*) => ExecuteCommand(OpenNewWindowForActiveApp)) +Hotkey("e", (*) => ExecuteCommand(OpenConfigFile)) +Hotkey("Esc", ClearReloadMode) +HotIf + +if Config.Has("config_watch") && Config["config_watch"]["enabled"] + StartConfigWatcher(config_path, Config["config_watch"]["interval_ms"]) SetWinDelay(-1) @@ -112,6 +103,10 @@ DefaultConfig() { "include_minimized", true, "close_on_focus_loss", true ), + "config_watch", Map( + "enabled", false, + "interval_ms", 2500 + ), "window_manager", Map( "grid_size", 3, "margins", Map( @@ -166,15 +161,9 @@ DefaultConfig() { "enabled", true, "overlay_opacity", 200 ), - "reload", Map( - "enabled", true, - "hotkey", "r", - "super_key_required", true, - "watch_enabled", false, - "watch_interval_ms", 1000, - "mode_enabled", true, - "mode_hotkey", ";", - "mode_timeout_ms", 20000 + "config_watch", Map( + "enabled", false, + "interval_ms", 2500 ) ) } diff --git a/src/hotkeys/unbound.ahk b/src/hotkeys/unbound.ahk index 12743cb..5eb3730 100644 --- a/src/hotkeys/unbound.ahk +++ b/src/hotkeys/unbound.ahk @@ -16,10 +16,6 @@ RegisterUnboundHotkeys() { AddUsedKey(used_keys, "o") AddUsedKey(used_keys, "n") - reload_config := Config["reload"] - if reload_config["enabled"] && reload_config["super_key_required"] - AddUsedKey(used_keys, reload_config["hotkey"]) - for _, app in Config["apps"] { if app.Has("hotkey") && app["hotkey"] != "" AddUsedKey(used_keys, app["hotkey"]) diff --git a/src/hotkeys/window.ahk b/src/hotkeys/window.ahk index 28f6269..ca764c2 100644 --- a/src/hotkeys/window.ahk +++ b/src/hotkeys/window.ahk @@ -507,9 +507,7 @@ OnSuperKeyUp() { UpdateCommandToastVisibility() } -if (move_mode_enabled || Config["reload"]["mode_enabled"] || Config["helper"]["enabled"]) { - RegisterSuperKeyHotkey("", " up", (*) => OnSuperKeyUp()) -} +RegisterSuperKeyHotkey("", " up", (*) => OnSuperKeyUp()) Hotkey("~LButton", (*) => BeginSuperDrag()) diff --git a/src/lib/command_toast.ahk b/src/lib/command_toast.ahk index e8db702..277f5a0 100644 --- a/src/lib/command_toast.ahk +++ b/src/lib/command_toast.ahk @@ -387,7 +387,7 @@ BuildCommandToastRows(key_width := 16) { } rows.Push(Map("key", "", "desc", "")) rows.Push(Map("key", "Command Mode", "desc", "")) - rows.Push(Map("key", Config["reload"]["mode_hotkey"], "desc", "enter command mode")) + rows.Push(Map("key", ";", "desc", "enter command mode")) return rows } diff --git a/src/lib/config_loader.ahk b/src/lib/config_loader.ahk index 208df5f..62b4675 100644 --- a/src/lib/config_loader.ahk +++ b/src/lib/config_loader.ahk @@ -156,15 +156,9 @@ ConfigSchema() { "enabled", "bool", "overlay_opacity", "number" ), - "reload", Map( + "config_watch", Map( "enabled", "bool", - "hotkey", "string", - "super_key_required", "bool", - "watch_enabled", "bool", - "watch_interval_ms", "number", - "mode_enabled", "bool", - "mode_hotkey", "string", - "mode_timeout_ms", "number" + "interval_ms", "number" ) ) } @@ -184,6 +178,7 @@ NormalizeSuperKeyConfig(config) { config["super_key"] := [super_value] } + NormalizeVirtualDesktopConfig(config) { if !config.Has("virtual_desktop") || !(config["virtual_desktop"] is Map) return From d05d22d4ce4f5133f31d69de044aefc5cbe30ff6 Mon Sep 17 00:00:00 2001 From: Grant Kilber Date: Sat, 7 Feb 2026 15:25:02 -0600 Subject: [PATCH 04/20] fix: app cycling now cycles through more than two tiles --- AGENTS.md | 3 +++ src/hotkeys/window.ahk | 29 ++++++++++++++++------------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4ecc7d8..ee75257 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,6 +32,9 @@ Single test / focused run: - If virtual desktops touched: test `super+alt+h/l`, mapped desktop hotkeys, and tray indicator. ## Code Style Guidelines + +Comment the "why" in and around code to help provide context from the instructions and conversations leading to changes. + ### AutoHotkey version and file headers - Use AutoHotkey v2 syntax and conventions. - Virtual desktop integration requires AutoHotkey v2.1 alpha (VD.ahk dependency). diff --git a/src/hotkeys/window.ahk b/src/hotkeys/window.ahk index ca764c2..8e40b8e 100644 --- a/src/hotkeys/window.ahk +++ b/src/hotkeys/window.ahk @@ -252,25 +252,28 @@ ApplyCurrentDesktopOrder(exe, win_list, active_hwnd, current_only := false) { return win_list } - if current_only - return current_order + if current_only { + ; Keep stable ordering to avoid z-order oscillation while cycling. + ordered := [] + for _, id in win_list { + if current_set.Has(id) + ordered.Push(id) + } + return ordered + } if !Config["virtual_desktop"]["cycle_prefer_current"] return win_list + ; Preserve win_list order while preferring current desktop windows first. ordered := [] - for _, id in current_order - ordered.Push(id) - - seen := Map() - for _, id in current_order - seen[id] := true - for _, id in win_list { - if seen.Has(id) - continue - ordered.Push(id) - seen[id] := true + if current_set.Has(id) + ordered.Push(id) + } + for _, id in win_list { + if !current_set.Has(id) + ordered.Push(id) } return ordered } From 5512bccd554e889fb68c8d7fe590b7124cf9bea9 Mon Sep 17 00:00:00 2001 From: Grant Kilber Date: Sat, 7 Feb 2026 15:29:04 -0600 Subject: [PATCH 05/20] adds some commentary --- harken.ahk | 1 + src/hotkeys/window.ahk | 2 ++ src/lib/command_toast.ahk | 2 ++ src/lib/virtual_desktop.ahk | 5 +++++ 4 files changed, 10 insertions(+) diff --git a/harken.ahk b/harken.ahk index 915eca8..76cc76e 100644 --- a/harken.ahk +++ b/harken.ahk @@ -427,6 +427,7 @@ MatchAppWindow(app, hwnd := 0) { MatchWindowFields(match, hwnd) { if !WinExist("ahk_id " hwnd) return false + ; Window handles can go stale during app switching; guard WinGet* calls. try exe_name := WinGetProcessName("ahk_id " hwnd) catch return false diff --git a/src/hotkeys/window.ahk b/src/hotkeys/window.ahk index 8e40b8e..cf66319 100644 --- a/src/hotkeys/window.ahk +++ b/src/hotkeys/window.ahk @@ -246,6 +246,7 @@ ApplyCurrentDesktopOrder(exe, win_list, active_hwnd, current_only := false) { for _, id in current_order current_set[id] := true + ; Current-desktop ordering uses visible windows to avoid skipping local tiles. if (current_order.Length = 0) { if (current_only && active_hwnd && WindowExistsAcrossDesktops(active_hwnd)) return [active_hwnd] @@ -457,6 +458,7 @@ BuildCycleWindowList(exe, win_list) { if !VirtualDesktopEnabled() return win_list + ; Cache app windows seen across desktops since WinGetList can hide off-desktop windows. cache := UpdateAppCycleCache(exe, win_list) if (cache.Count = 0) return win_list diff --git a/src/lib/command_toast.ahk b/src/lib/command_toast.ahk index 277f5a0..b57fdd7 100644 --- a/src/lib/command_toast.ahk +++ b/src/lib/command_toast.ahk @@ -217,6 +217,7 @@ ToggleCommandHelper() { ShowCommandToastTemporary() { global command_helper_enabled, command_toast_temp_visible + ; Allow the temporary overlay even if persistent helper overlay is disabled. command_toast_temp_visible := true ShowCommandToast(true) StartCommandToastInputHook() @@ -284,6 +285,7 @@ CommandToastKeydownHandler(*) { CommandToastOnKeyDown(*) { if ReloadModeActive() || Window.IsMoveMode() return + ; Any input dismisses the temporary overlay in normal mode. HideCommandToast() } diff --git a/src/lib/virtual_desktop.ahk b/src/lib/virtual_desktop.ahk index 29106c0..5239818 100644 --- a/src/lib/virtual_desktop.ahk +++ b/src/lib/virtual_desktop.ahk @@ -40,6 +40,7 @@ InitVirtualDesktopAutoAssign() { return if !Config["virtual_desktop"].Has("auto_assign") || !Config["virtual_desktop"]["auto_assign"] return + ; Polling watcher only handles newly created windows; users can move later. interval := 500 if Config["virtual_desktop"].Has("auto_assign_interval_ms") interval := Config["virtual_desktop"]["auto_assign_interval_ms"] @@ -70,6 +71,7 @@ VirtualDesktopAutoAssignTick(*) { if seen_hwnds.Has(hwnd) continue seen_hwnds[hwnd] := true + ; Assign once per window so manual moves are respected afterward. TryAutoAssignWindow(hwnd) } @@ -102,6 +104,7 @@ TryAutoAssignWindow(hwnd) { return total := VD.getCount() if (target_desktop > total) { + ; Ensure the destination desktop exists before moving the window. VD.createUntil(target_desktop) VD.IVirtualDesktopListChanged() total := VD.getCount() @@ -148,6 +151,7 @@ AppConfigIgnoresWindow(app, hwnd) { InitVirtualDesktopTrayIndicator() { if !VirtualDesktopTrayEnabled() return + ; Update via VD notifications to avoid timer polling. UpdateVirtualDesktopTrayIndicator() VD.ListenersCurrentVirtualDesktopChanged[UpdateVirtualDesktopTrayIndicator] := true } @@ -179,6 +183,7 @@ FormatVirtualDesktopTrayText(current, total) { SetTrayIconText(text) { if (text = "") return + ; Draw text onto the existing AHK tray icon (current/total). hicon := CreateTextTrayIcon(text) if !hicon return From a00b11814d3141b81f556dbe5e8aabd47c2a4fe0 Mon Sep 17 00:00:00 2001 From: Grant Kilber Date: Sat, 14 Feb 2026 12:13:36 -0600 Subject: [PATCH 06/20] adds comments --- harken.ahk | 1 + src/hotkeys/apps.ahk | 1 + src/hotkeys/directional_focus.ahk | 1 + src/hotkeys/global_hotkey.ahk | 1 + src/hotkeys/unbound.ahk | 1 + src/hotkeys/window.ahk | 1 + src/hotkeys/window_walker.ahk | 1 + src/lib/JXON.ahk | 1 + src/lib/VD.ahk | 3 ++- src/lib/command_toast.ahk | 1 + src/lib/config_loader.ahk | 1 + src/lib/device_detect.ahk | 1 + src/lib/directional_focus.ahk | 1 + src/lib/focus_border.ahk | 1 + src/lib/focus_or_run.ahk | 1 + src/lib/state_store.ahk | 1 + src/lib/toml.ahk | 1 + src/lib/virtual_desktop.ahk | 1 + src/lib/window_inspector.ahk | 2 ++ src/lib/window_manager.ahk | 1 + src/lib/window_walker.ahk | 1 + 21 files changed, 23 insertions(+), 1 deletion(-) diff --git a/harken.ahk b/harken.ahk index 76cc76e..78892c5 100644 --- a/harken.ahk +++ b/harken.ahk @@ -1,4 +1,5 @@ #Requires AutoHotkey v2.0 +; Entry point: loads config, initializes modules, registers hotkeys. #SingleInstance #Include src/lib/JXON.ahk diff --git a/src/hotkeys/apps.ahk b/src/hotkeys/apps.ahk index 2beea09..4b0d6c9 100644 --- a/src/hotkeys/apps.ahk +++ b/src/hotkeys/apps.ahk @@ -1,3 +1,4 @@ +; App hotkey bindings (launch or focus). global Config HotIf IsSuperKeyPressed diff --git a/src/hotkeys/directional_focus.ahk b/src/hotkeys/directional_focus.ahk index 45d90ba..52763dc 100644 --- a/src/hotkeys/directional_focus.ahk +++ b/src/hotkeys/directional_focus.ahk @@ -1,3 +1,4 @@ +; Directional focus hotkey bindings. global Config if Config.Has("directional_focus") && Config["directional_focus"]["enabled"] { diff --git a/src/hotkeys/global_hotkey.ahk b/src/hotkeys/global_hotkey.ahk index 02f02d7..a31842c 100644 --- a/src/hotkeys/global_hotkey.ahk +++ b/src/hotkeys/global_hotkey.ahk @@ -1,3 +1,4 @@ +; Global hotkey bindings (super + key combos). global Config for _, hotkey_config in Config["global_hotkeys"] { diff --git a/src/hotkeys/unbound.ahk b/src/hotkeys/unbound.ahk index 5eb3730..7a4d2cf 100644 --- a/src/hotkeys/unbound.ahk +++ b/src/hotkeys/unbound.ahk @@ -1,3 +1,4 @@ +; Unbound hotkey helper + reserved key detection. global Config RegisterUnboundHotkeys() { diff --git a/src/hotkeys/window.ahk b/src/hotkeys/window.ahk index cf66319..1773967 100644 --- a/src/hotkeys/window.ahk +++ b/src/hotkeys/window.ahk @@ -5,6 +5,7 @@ move_step := Config["window"]["move_step"] super_double_tap_ms := Config["window"]["super_double_tap_ms"] move_mode_enabled := Config["window"]["move_mode"]["enable"] move_mode_cancel_key := Config["window"]["move_mode"]["cancel_key"] +; Window hotkeys (resize/move/cycle) and virtual desktop bindings. center_cycle_hotkey := Config["window"]["center_width_cycle_hotkey"] cycle_app_windows_hotkey := Config["window"]["cycle_app_windows_hotkey"] cycle_app_windows_current_hotkey := Config["window"]["cycle_app_windows_current_hotkey"] diff --git a/src/hotkeys/window_walker.ahk b/src/hotkeys/window_walker.ahk index e406cc7..e3d6858 100644 --- a/src/hotkeys/window_walker.ahk +++ b/src/hotkeys/window_walker.ahk @@ -1,3 +1,4 @@ +; Window walker hotkey binding. global Config if Config.Has("window_selector") && Config["window_selector"]["enabled"] { diff --git a/src/lib/JXON.ahk b/src/lib/JXON.ahk index a3b55a9..a3774d2 100644 --- a/src/lib/JXON.ahk +++ b/src/lib/JXON.ahk @@ -1,4 +1,5 @@ ;;;; AHK v2 +; Third-party: JSON serialization helpers (JXON). See LICENSES/JXON_ahk2-LICENSE.md ; Example =================================================================================== ; =========================================================================================== diff --git a/src/lib/VD.ahk b/src/lib/VD.ahk index 01c5ac4..4db1a12 100644 --- a/src/lib/VD.ahk +++ b/src/lib/VD.ahk @@ -1,5 +1,6 @@ #requires AutoHotkey v2.1-alpha.5 +; Third-party: Virtual desktop COM wrapper. See LICENSES/VD.ahk-LICENSE.md class VD { class Array { static find(arr, callback) { @@ -1228,4 +1229,4 @@ class VD { } } -} \ No newline at end of file +} diff --git a/src/lib/command_toast.ahk b/src/lib/command_toast.ahk index b57fdd7..f875abf 100644 --- a/src/lib/command_toast.ahk +++ b/src/lib/command_toast.ahk @@ -1,4 +1,5 @@ global Config, AppState +; Command overlay UI + helper toggle state. global command_helper_enabled := false global command_toast_gui := "" global command_toast_text := "" diff --git a/src/lib/config_loader.ahk b/src/lib/config_loader.ahk index 62b4675..68213b1 100644 --- a/src/lib/config_loader.ahk +++ b/src/lib/config_loader.ahk @@ -1,3 +1,4 @@ +; Config loading + schema validation + normalization. LoadConfig(config_path, default_config := Map()) { config := CloneMap(default_config) errors := [] diff --git a/src/lib/device_detect.ahk b/src/lib/device_detect.ahk index a4a1c9e..5be4854 100644 --- a/src/lib/device_detect.ahk +++ b/src/lib/device_detect.ahk @@ -1,3 +1,4 @@ +; Device detection helpers (keyboard presence). IsKeyboardPresent(vidPid) { try { diff --git a/src/lib/directional_focus.ahk b/src/lib/directional_focus.ahk index c56182f..d2c9a86 100644 --- a/src/lib/directional_focus.ahk +++ b/src/lib/directional_focus.ahk @@ -1,3 +1,4 @@ +; Directional focus calculations and stacked cycling. global Config global directional_focus_history := Map() global directional_focus_debug_enabled := false diff --git a/src/lib/focus_border.ahk b/src/lib/focus_border.ahk index 36c4038..3c32ea1 100644 --- a/src/lib/focus_border.ahk +++ b/src/lib/focus_border.ahk @@ -3,6 +3,7 @@ ; For Windows 11 (24H2) ; Runs continuously in the background. ; Adjust the border color, thickness, and corner roundness below. +; Why: give a visible focus cue, plus mode-specific colors. ; ================================ global Config diff --git a/src/lib/focus_or_run.ahk b/src/lib/focus_or_run.ahk index 932e078..dffcb9c 100644 --- a/src/lib/focus_or_run.ahk +++ b/src/lib/focus_or_run.ahk @@ -1,4 +1,5 @@ ; Helper: focus existing window, otherwise run app. +; Launch-or-focus helpers + per-app matching. ; If the target window is already active, toggle back to the previous window ; recorded for this hotkey. FocusOrRun(winTitle, exePath, hotkey_id, app_config := "", *) { diff --git a/src/lib/state_store.ahk b/src/lib/state_store.ahk index cec2d8a..1beaf1e 100644 --- a/src/lib/state_store.ahk +++ b/src/lib/state_store.ahk @@ -13,6 +13,7 @@ LoadState() { return Map() } +; Persist small runtime state (overlay toggle, etc.). SaveState(state) { if !(state is Map) return diff --git a/src/lib/toml.ahk b/src/lib/toml.ahk index 167ce79..c128681 100644 --- a/src/lib/toml.ahk +++ b/src/lib/toml.ahk @@ -1,3 +1,4 @@ +; Minimal TOML parser/loader used for config. TomlLoadFile(file_path) { toml_text := FileRead(file_path, "UTF-8") return TomlParse(toml_text) diff --git a/src/lib/virtual_desktop.ahk b/src/lib/virtual_desktop.ahk index 5239818..de17faf 100644 --- a/src/lib/virtual_desktop.ahk +++ b/src/lib/virtual_desktop.ahk @@ -1,5 +1,6 @@ #Include %A_LineFile%\..\VD.ahk +; Virtual desktop integration (tray indicator, auto-assign, helpers). VirtualDesktopEnabled() { global Config if !IsSet(Config) diff --git a/src/lib/window_inspector.ahk b/src/lib/window_inspector.ahk index 984e2b2..12d5f24 100644 --- a/src/lib/window_inspector.ahk +++ b/src/lib/window_inspector.ahk @@ -1,5 +1,6 @@ #Requires AutoHotkey v2.0 +; Window inspector UI for titles/classes/PIDs. ShowWindowInspector() { static inspector_gui := "" static window_list := "" @@ -15,6 +16,7 @@ ShowWindowInspector() { inspector_gui := Gui("+Resize", "harken Window Inspector") inspector_gui.SetFont("s10", "Segoe UI") + ; Include active column and auto-refresh to reflect focus changes. window_list := inspector_gui.AddListView("w1020 r26 Grid", ["Active", "Title", "Exe", "Class", "PID", "HWND"]) window_list.ModifyCol(1, 60) window_list.ModifyCol(2, 380) diff --git a/src/lib/window_manager.ahk b/src/lib/window_manager.ahk index 6cb41b7..66ca3ef 100644 --- a/src/lib/window_manager.ahk +++ b/src/lib/window_manager.ahk @@ -16,6 +16,7 @@ global Config +; Window manager utilities: grid math, move/resize, exceptions. class Window { ;------------------------------------------------------------------------------- diff --git a/src/lib/window_walker.ahk b/src/lib/window_walker.ahk index 6d65b07..dfc40bb 100644 --- a/src/lib/window_walker.ahk +++ b/src/lib/window_walker.ahk @@ -1,5 +1,6 @@ global Config +; Window selector UI (fuzzy list + activation). class WindowWalker { static gui := "" From 91afeefb8e8fa5243c33414297ec6360423b78f8 Mon Sep 17 00:00:00 2001 From: Grant Kilber Date: Sat, 14 Feb 2026 19:12:52 -0600 Subject: [PATCH 07/20] warning on config issues instead of error --- harken.ahk | 47 +++++++++++++++++++++++++++++++++++++++ src/lib/config_loader.ahk | 27 +++++++++++----------- 2 files changed, 61 insertions(+), 13 deletions(-) diff --git a/harken.ahk b/harken.ahk index 78892c5..0304ef2 100644 --- a/harken.ahk +++ b/harken.ahk @@ -18,11 +18,17 @@ EnsureConfigExists(config_path, DefaultConfig()) config_result := LoadConfig(config_path, DefaultConfig()) global Config := config_result["config"] config_errors := config_result["errors"] +config_warnings := config_result["warnings"] if (config_errors.Length) { appdata_dir := GetAppDataDir() LogConfigErrors(config_errors, appdata_dir "\config.errors.log", config_path) return } +if (config_warnings.Length) { + appdata_dir := GetAppDataDir() + LogConfigWarnings(config_warnings, appdata_dir "\config.warnings.log", config_path) + ShowConfigWarnings(config_warnings, appdata_dir "\config.warnings.log") +} InitVirtualDesktop() global AppState := LoadState() InitCommandToast() @@ -191,6 +197,47 @@ LogConfigErrors(errors, log_path, config_path := "") { ShowConfigErrorsGui(details, log_path) } +LogConfigWarnings(warnings, log_path, config_path := "") { + DirCreate(GetAppDataDir()) + header := "[" A_Now "] Config warnings:" "`n" + FileAppend(header, log_path) + if (config_path != "") + FileAppend("Config: " config_path "`n", log_path) + for _, warning in warnings { + FileAppend("- " warning "`n", log_path) + } + FileAppend("`n", log_path) +} + +ShowConfigWarnings(warnings, log_path) { + static warning_gui := "" + if warning_gui { + warning_gui.Destroy() + warning_gui := "" + } + + warning_gui := Gui("+AlwaysOnTop +ToolWindow", "Harken Config Warnings") + warning_gui.SetFont("s10", "Segoe UI") + warning_gui.AddText("xm", "Unknown config keys detected. See log for details.") + + preview := "" + max_lines := 8 + for i, warning in warnings { + if (i > max_lines) { + preview .= "..." "`n" + break + } + preview .= warning "`n" + } + warning_gui.AddEdit("xm w520 r" max_lines " ReadOnly", preview) + + open_btn := warning_gui.AddButton("xm y+10 w120", "Open Log") + open_btn.OnEvent("Click", (*) => Run(log_path)) + close_btn := warning_gui.AddButton("x+10 yp w120", "Close") + close_btn.OnEvent("Click", (*) => warning_gui.Destroy()) + warning_gui.Show() +} + ShowConfigErrorsGui(details, log_path) { error_gui := Gui("+AlwaysOnTop", "harken Config Errors") error_gui.SetFont("s10", "Segoe UI") diff --git a/src/lib/config_loader.ahk b/src/lib/config_loader.ahk index 68213b1..44ef6f2 100644 --- a/src/lib/config_loader.ahk +++ b/src/lib/config_loader.ahk @@ -2,6 +2,7 @@ LoadConfig(config_path, default_config := Map()) { config := CloneMap(default_config) errors := [] + warnings := [] if FileExist(config_path) { try { @@ -11,21 +12,23 @@ LoadConfig(config_path, default_config := Map()) { errors.Push("config.parse: " err.Message) return Map( "config", config, - "errors", errors + "errors", errors, + "warnings", warnings ) } } NormalizeSuperKeyConfig(config) NormalizeVirtualDesktopConfig(config) - errors := ValidateConfig(config, ConfigSchema()) + ValidateConfig(config, ConfigSchema(), errors, warnings) ValidateSuperKeys(config, errors) ValidateApps(config, errors) ValidateVirtualDesktopHotkeys(config, errors) return Map( "config", config, - "errors", errors + "errors", errors, + "warnings", warnings ) } @@ -164,10 +167,8 @@ ConfigSchema() { ) } -ValidateConfig(config, schema) { - errors := [] - ValidateNode(config, schema, "config", errors) - return errors +ValidateConfig(config, schema, errors, warnings) { + ValidateNode(config, schema, "config", errors, warnings) } NormalizeSuperKeyConfig(config) { @@ -299,12 +300,12 @@ ValidateVirtualDesktopHotkeys(config, errors) { } } -ValidateNode(value, spec, path, errors) { +ValidateNode(value, spec, path, errors, warnings) { if (spec is Map) { if spec.Has("__optional__") { if (value = "") return - return ValidateNode(value, spec["spec"], path, errors) + return ValidateNode(value, spec["spec"], path, errors, warnings) } if !(value is Map) { errors.Push(path " should be an object") @@ -321,13 +322,13 @@ ValidateNode(value, spec, path, errors) { for key, val in value { if !spec.Has(key) { - errors.Push(path "." key " is unknown") + warnings.Push(path "." key " is unknown") continue } if (spec[key] is Map && spec[key].Has("__optional__") && spec[key]["__optional__"]) { - ValidateNode(val, spec[key]["spec"], path "." key, errors) + ValidateNode(val, spec[key]["spec"], path "." key, errors, warnings) } else { - ValidateNode(val, spec[key], path "." key, errors) + ValidateNode(val, spec[key], path "." key, errors, warnings) } } return @@ -342,7 +343,7 @@ ValidateNode(value, spec, path, errors) { return item_spec := spec[1] for i, item in value { - ValidateNode(item, item_spec, path "[" i "]", errors) + ValidateNode(item, item_spec, path "[" i "]", errors, warnings) } return } From 6e8e59f767b851feb9b7de3eb129c9ec06cc1a08 Mon Sep 17 00:00:00 2001 From: Grant Kilber Date: Sat, 14 Feb 2026 19:22:14 -0600 Subject: [PATCH 08/20] change apps config --- config/config.example.toml | 29 ++++++++++++----------------- src/lib/config_loader.ahk | 31 +++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/config/config.example.toml b/config/config.example.toml index 64a98d9..522d30a 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -2,8 +2,10 @@ config_version = 1 # Use one or more keys as super (CapsLock is the default). super_key = ["F24", "CapsLock"] -[[apps]] -id = "browser" +[apps] +# Named tables: key is the app id; do not set `id` inside. + +[apps.browser] hotkey = "f" win_title = "ahk_exe floorp.exe" run = "floorp" @@ -11,37 +13,32 @@ run = "floorp" # follow_on_spawn = true run_paths = [] -[[apps]] -id = "files" +[apps.files] hotkey = "e" win_title = "ahk_exe explorer.exe" run = "explorer" run_paths = [] -[[apps]] -id = "editor" +[apps.editor] hotkey = "v" win_title = "ahk_exe Code.exe" run = "code" # run_start_in = "C:\\projects" run_paths = [] -[[apps]] -id = "terminal" +[apps.terminal] hotkey = "s" win_title = "ahk_exe WindowsTerminal.exe" run = "wt" run_paths = [] -[[apps]] -id = "notepad" +[apps.notepad] hotkey = "n" win_title = "ahk_exe notepad++.exe" run = "notepad++" run_paths = [] -[[apps]] -id = "chat" +[apps.chat] hotkey = "t" win_title = "ahk_exe telegram.exe" run = "telegram" @@ -49,19 +46,17 @@ run_paths = [] # desktop = 2 # ignore_classes = ["Qt51518QWindowToolSaveBits"] -[[apps]] -id = "discord" +[apps.discord] hotkey = "d" win_title = "ahk_exe discord.exe" run = "discord" run_paths = [] # desktop = 2 -[[apps]] -id = "vscode-border" +[apps.vscode-border] match = { exe = "Code.exe", title = " - Visual Studio Code$", title_regex = true } # Optional per-app focus border tweaks. -focus_border = { border_color = "#4D96FF", border_thickness = 3, corner_radius = 12lll } +focus_border = { border_color = "#4D96FF", border_thickness = 3, corner_radius = 12 } [[global_hotkeys]] enabled = true diff --git a/src/lib/config_loader.ahk b/src/lib/config_loader.ahk index 44ef6f2..010dd34 100644 --- a/src/lib/config_loader.ahk +++ b/src/lib/config_loader.ahk @@ -20,6 +20,7 @@ LoadConfig(config_path, default_config := Map()) { NormalizeSuperKeyConfig(config) NormalizeVirtualDesktopConfig(config) + NormalizeAppsConfig(config, errors) ValidateConfig(config, ConfigSchema(), errors, warnings) ValidateSuperKeys(config, errors) ValidateApps(config, errors) @@ -180,6 +181,36 @@ NormalizeSuperKeyConfig(config) { config["super_key"] := [super_value] } +NormalizeAppsConfig(config, errors) { + if !config.Has("apps") + return + + apps := config["apps"] + if (apps is Array) + return + + if !(apps is Map) { + errors.Push("config.apps should be an array or object") + return + } + + normalized := [] + for app_id, app_config in apps { + if !(app_config is Map) { + errors.Push("config.apps." app_id " should be an object") + continue + } + if app_config.Has("id") { + errors.Push("config.apps." app_id " should not set id when using named tables") + app_config.Delete("id") + } + app_config["id"] := app_id + normalized.Push(app_config) + } + + config["apps"] := normalized +} + NormalizeVirtualDesktopConfig(config) { if !config.Has("virtual_desktop") || !(config["virtual_desktop"] is Map) From adf3e124858e1ada4263ae150a8f6cb4a6e01a13 Mon Sep 17 00:00:00 2001 From: Grant Kilber Date: Tue, 17 Feb 2026 15:04:41 -0600 Subject: [PATCH 09/20] [command]: send tiles to assigned desktops --- README.md | 18 +++++++++++- config/config.example.toml | 1 + harken.ahk | 36 ++++++++++++++++++++--- src/hotkeys/window.ahk | 57 ++++++++++++++++++++++++++++++++++-- src/lib/command_toast.ahk | 3 +- src/lib/config_loader.ahk | 1 + src/lib/focus_or_run.ahk | 23 ++++++++++++--- src/lib/virtual_desktop.ahk | 51 ++++++++++++++++++++++++++++++++ src/lib/window_inspector.ahk | 30 +++++++++++++++++-- 9 files changed, 205 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index b75a6f2..1cdddc9 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ Enter Command Mode with `super + ;`. - `r` to reload program/config - `e` to open config file - `w` opens a new window for the active program, if the program supports it +- `g` reapply app desktop assignments - `n` toggles the command overlay on or off - `i` opens the [Helper Utility](#helper-utility) @@ -101,6 +102,21 @@ Enter Command Mode with `super + ;`. - Press `e` to open the config file. You can also find it manually in `~/.config/harken/harken.toml` as it will be created on first run. - After making changes to your config you can reload (the entire program) with `r` while in command mode. +## Window Matching + +App entries under `apps` can match windows via `win_title` or a `match` map. The `match` map accepts +`exe`, `class`, and `title`, plus `*_regex = true` to treat the value as a regex. + +Use `exclude_titles` to ignore specific window titles for a given app. Each entry is a regex pattern +and the match is case-insensitive unless you include your own `(?i)` prefix. + +```toml +[apps.editor] +hotkey = "v" +match = { exe = "Code.exe", title = " - Visual Studio Code$", title_regex = true } +exclude_titles = ["^Settings$", "^Welcome$"] +``` + ### All default keybindings #### Window management (Super) @@ -113,7 +129,7 @@ Enter Command Mode with `super + ;`. | `super + shift + c` | Cycle app windows on current desktop | | `super + space` | Center width cycle | | `super + m` | Maximize/un-maximize | -| `super + q` | Close window | +| `alt + q` | Close window | | `super + Left/Right/Up/Down` | Resize window and snap to grids | | `super + shift + h/j/k/l` | Resize centered | | `super + ctrl + h/j/k/l` | Move window | diff --git a/config/config.example.toml b/config/config.example.toml index 522d30a..b26c1ce 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -51,6 +51,7 @@ hotkey = "d" win_title = "ahk_exe discord.exe" run = "discord" run_paths = [] +# exclude_titles = ["^Discord Updater$", "^Discord Canary$"] # desktop = 2 [apps.vscode-border] diff --git a/harken.ahk b/harken.ahk index 0304ef2..836c7cc 100644 --- a/harken.ahk +++ b/harken.ahk @@ -49,6 +49,7 @@ Hotkey("r", (*) => ExecuteCommand(() => Reload())) Hotkey("i", (*) => ExecuteCommand(OpenWindowInspector)) Hotkey("n", (*) => ExecuteCommand(ToggleCommandHelper)) Hotkey("w", (*) => ExecuteCommand(OpenNewWindowForActiveApp)) +Hotkey("g", (*) => ExecuteCommand(ReapplyDesktopAssignments)) Hotkey("e", (*) => ExecuteCommand(OpenConfigFile)) Hotkey("Esc", ClearReloadMode) HotIf @@ -74,10 +75,10 @@ DefaultConfig() { "config_version", 1, "super_key", ["CapsLock"], "apps", [ - Map("id", "files", "hotkey", "e", "win_title", "ahk_exe explorer.exe", "run", "explorer"), - Map("id", "editor", "hotkey", "v", "win_title", "ahk_exe Code.exe", "run", "code"), - Map("id", "terminal", "hotkey", "s", "win_title", "ahk_exe WindowsTerminal.exe", "run", "wt"), - Map("id", "notes", "hotkey", "n", "win_title", "ahk_exe notepad++.exe", "run", "notepad++") + Map("id", "files", "hotkey", "e", "win_title", "ahk_exe explorer.exe", "exclude_titles", [], "run", "explorer"), + Map("id", "editor", "hotkey", "v", "win_title", "ahk_exe Code.exe", "exclude_titles", [], "run", "code"), + Map("id", "terminal", "hotkey", "s", "win_title", "ahk_exe WindowsTerminal.exe", "exclude_titles", [], "run", "wt"), + Map("id", "notes", "hotkey", "n", "win_title", "ahk_exe notepad++.exe", "exclude_titles", [], "run", "notepad++") ], "global_hotkeys", [ Map( @@ -459,6 +460,9 @@ MatchAppWindow(app, hwnd := 0) { if !hwnd return false + if AppConfigExcludesTitle(app, hwnd) + return false + if app.Has("match") && (app["match"] is Map) return MatchWindowFields(app["match"], hwnd) @@ -472,6 +476,30 @@ MatchAppWindow(app, hwnd := 0) { return false } +AppConfigExcludesTitle(app, hwnd) { + ; Exclude helper windows that should not be matched to an app. + if !(app is Map) + return false + if !app.Has("exclude_titles") || !(app["exclude_titles"] is Array) + return false + + title := "" + try title := WinGetTitle("ahk_id " hwnd) + catch + return false + + for _, pattern in app["exclude_titles"] { + if (pattern = "") + continue + if !RegExMatch(pattern, "^\(\?i\)") + pattern := "(?i)" pattern + if RegExMatch(title, pattern) + return true + } + + return false +} + MatchWindowFields(match, hwnd) { if !WinExist("ahk_id " hwnd) return false diff --git a/src/hotkeys/window.ahk b/src/hotkeys/window.ahk index 1773967..c7964fc 100644 --- a/src/hotkeys/window.ahk +++ b/src/hotkeys/window.ahk @@ -210,6 +210,32 @@ FilterWindowList(exe, list) { return filtered } +FilterExcludedCycleWindows(list, app_config := "") { + if !(app_config is Map) + return list + if !app_config.Has("exclude_titles") || !(app_config["exclude_titles"] is Array) + return list + + filtered := [] + for _, id in list { + if !AppConfigExcludesTitle(app_config, id) + filtered.Push(id) + } + return filtered +} + +FilterAppConfigCycleWindows(list, app_config := "") { + if !(app_config is Map) + return list + + filtered := [] + for _, id in list { + if AppConfigMatchesWindow(app_config, id) + filtered.Push(id) + } + return filtered +} + ApplyDesktopCyclePreference(list, current_only := false) { if !VirtualDesktopEnabled() return list @@ -616,9 +642,21 @@ CycleAppWindows(*) { if !exe return - win_list := GetWindowsAcrossDesktops("ahk_exe " exe) + app_config := FindAppConfigByWindow(hwnd) + if (app_config is Map && AppConfigExcludesTitle(app_config, hwnd)) + return + + win_list := [] + if (app_config is Map) { + win_title := app_config.Has("win_title") ? app_config["win_title"] : "" + win_list := GetAppWindowList(win_title, app_config) + } else { + win_list := GetWindowsAcrossDesktops("ahk_exe " exe) + } win_list := FilterWindowList(exe, win_list) win_list := BuildCycleWindowList(exe, win_list) + win_list := FilterAppConfigCycleWindows(win_list, app_config) + win_list := FilterExcludedCycleWindows(win_list, app_config) if (win_list.Length < 2) return @@ -670,9 +708,21 @@ CycleAppWindowsCurrent(*) { if !exe return - win_list := GetWindowsAcrossDesktops("ahk_exe " exe) + app_config := FindAppConfigByWindow(hwnd) + if (app_config is Map && AppConfigExcludesTitle(app_config, hwnd)) + return + + win_list := [] + if (app_config is Map) { + win_title := app_config.Has("win_title") ? app_config["win_title"] : "" + win_list := GetAppWindowList(win_title, app_config) + } else { + win_list := GetWindowsAcrossDesktops("ahk_exe " exe) + } win_list := FilterWindowList(exe, win_list) win_list := BuildCycleWindowList(exe, win_list) + win_list := FilterAppConfigCycleWindows(win_list, app_config) + win_list := FilterExcludedCycleWindows(win_list, app_config) if (win_list.Length < 2) return @@ -812,7 +862,6 @@ Hotkey("^j", (*) => MoveActiveWindow(0, move_step)) Hotkey("^k", (*) => MoveActiveWindow(0, -move_step)) HotIf IsSuperKeyPressed Hotkey("m", ToggleMaximize) -Hotkey("q", CloseWindow) Hotkey(cycle_app_windows_hotkey, CycleAppWindows) if (cycle_app_windows_current_hotkey != "") Hotkey(cycle_app_windows_current_hotkey, CycleAppWindowsCurrent) @@ -821,6 +870,8 @@ if (minimize_others_hotkey != "") Hotkey(minimize_others_hotkey, MinimizeOtherWindows) HotIf +Hotkey("!q", CloseWindow) + HotIf (*) => IsSuperKeyPressed() && IsAltPressed() && !GetKeyState("Shift", "P") if (vd_prev_hotkey != "") Hotkey(vd_prev_hotkey, (*) => ( diff --git a/src/lib/command_toast.ahk b/src/lib/command_toast.ahk index f875abf..ed9fa3a 100644 --- a/src/lib/command_toast.ahk +++ b/src/lib/command_toast.ahk @@ -345,6 +345,7 @@ BuildCommandToastModel() { lines.Push(FormatRow("i", "window inspector", key_width)) lines.Push(FormatRow("n", "toggle command overlay", key_width)) lines.Push(FormatRow("w", "new window (active app)", key_width)) + lines.Push(FormatRow("g", "reapply app desktops", key_width)) lines.Push(FormatRow("Esc/super", "exit command mode", key_width)) model["mode"] := "command" model["title"] := "Command Mode" @@ -370,7 +371,7 @@ BuildCommandToastRows(key_width := 16) { rows.Push(Map("key", "super+ctrl+h/j/k/l", "desc", "move")) rows.Push(Map("key", "super+m", "desc", "maximize")) rows.Push(Map("key", "alt+-", "desc", "minimize")) - rows.Push(Map("key", "super+q", "desc", "close")) + rows.Push(Map("key", "alt+q", "desc", "close")) rows.Push(Map("key", "super+" Config["window"]["cycle_app_windows_hotkey"], "desc", "cycle app windows")) if (Config["window"]["cycle_app_windows_current_hotkey"] != "") rows.Push(Map("key", "super+" Config["window"]["cycle_app_windows_current_hotkey"], "desc", "cycle app windows (current desktop)")) diff --git a/src/lib/config_loader.ahk b/src/lib/config_loader.ahk index 010dd34..e72a23a 100644 --- a/src/lib/config_loader.ahk +++ b/src/lib/config_loader.ahk @@ -55,6 +55,7 @@ ConfigSchema() { "desktop", OptionalSpec("number"), "follow_on_spawn", OptionalSpec("bool"), "ignore_classes", OptionalSpec(["string"]), + "exclude_titles", OptionalSpec(["string"]), "focus_border", OptionalSpec(Map( "border_color", OptionalSpec("string"), "move_mode_color", OptionalSpec("string"), diff --git a/src/lib/focus_or_run.ahk b/src/lib/focus_or_run.ahk index dffcb9c..a87b07d 100644 --- a/src/lib/focus_or_run.ahk +++ b/src/lib/focus_or_run.ahk @@ -36,22 +36,37 @@ FocusOrRun(winTitle, exePath, hotkey_id, app_config := "", *) { GetAppWindowList(win_title, app_config := "") { if (app_config is Map && app_config.Has("match") && app_config["match"] is Map) { - return GetWindowsByMatch(app_config["match"]) + return GetWindowsByMatch(app_config["match"], app_config) } if !win_title return [] - return GetWindowsAcrossDesktops(win_title) + return FilterExcludedWindows(GetWindowsAcrossDesktops(win_title), app_config) } -GetWindowsByMatch(match) { +GetWindowsByMatch(match, app_config := "") { matches := [] for _, hwnd in GetWindowsAcrossDesktops() { - if MatchWindowFields(match, hwnd) + if MatchWindowFields(match, hwnd) && !AppConfigExcludesTitle(app_config, hwnd) matches.Push(hwnd) } return matches } +FilterExcludedWindows(hwnds, app_config := "") { + if !(app_config is Map) + return hwnds + if !app_config.Has("exclude_titles") || !(app_config["exclude_titles"] is Array) + return hwnds + + filtered := [] + for _, hwnd in hwnds { + if !AppConfigExcludesTitle(app_config, hwnd) + filtered.Push(hwnd) + } + + return filtered +} + PickFocusableAppWindow(hwnds, win_title) { for hwnd in hwnds { if !WinExist("ahk_id " hwnd) diff --git a/src/lib/virtual_desktop.ahk b/src/lib/virtual_desktop.ahk index de17faf..e0ba21b 100644 --- a/src/lib/virtual_desktop.ahk +++ b/src/lib/virtual_desktop.ahk @@ -124,7 +124,58 @@ TryAutoAssignWindow(hwnd) { } } +ReapplyDesktopAssignments(*) { + if !VirtualDesktopEnabled() + return + win_list := GetWindowsAcrossDesktops() + for _, hwnd in win_list + EnforceAppDesktopAssignment(hwnd) +} + +EnforceAppDesktopAssignment(hwnd) { + if !WindowExistsAcrossDesktops(hwnd) + return false + try ex_style := WinGetExStyle("ahk_id " hwnd) + catch + return false + if (ex_style & 0x80) || (ex_style & 0x08000000) + return false + + for _, app in Config["apps"] { + if !(app is Map) + continue + if !app.Has("desktop") + continue + if !AppConfigMatchesWindow(app, hwnd) + continue + if AppConfigIgnoresWindow(app, hwnd) + continue + target_desktop := app["desktop"] + if (target_desktop <= 0) + return false + + assigned_desktop := GetWindowDesktopNum(hwnd) + if (assigned_desktop = target_desktop) + return true + + total := VD.getCount() + if (target_desktop > total) { + ; Ensure the destination desktop exists before moving the window. + VD.createUntil(target_desktop) + VD.IVirtualDesktopListChanged() + total := VD.getCount() + } + if (target_desktop > total) + return false + VD.MoveWindowToDesktopNum("ahk_id " hwnd, target_desktop, false) + return true + } + return false +} + AppConfigMatchesWindow(app, hwnd) { + if AppConfigExcludesTitle(app, hwnd) + return false if app.Has("match") && (app["match"] is Map) return MatchWindowFields(app["match"], hwnd) diff --git a/src/lib/window_inspector.ahk b/src/lib/window_inspector.ahk index 12d5f24..e08369c 100644 --- a/src/lib/window_inspector.ahk +++ b/src/lib/window_inspector.ahk @@ -5,11 +5,15 @@ ShowWindowInspector() { static inspector_gui := "" static window_list := "" static refresh_timer := 0 + static refresh_paused := true + static auto_refresh_btn := "" if inspector_gui { inspector_gui.Show() RefreshList(window_list) - StartAutoRefresh(window_list, &refresh_timer) + UpdateAutoRefreshButton(auto_refresh_btn, refresh_paused) + if !refresh_paused + StartAutoRefresh(window_list, &refresh_timer) return } @@ -37,11 +41,16 @@ ShowWindowInspector() { export_btn := inspector_gui.AddButton("x+10 yp w140", "Export File") export_btn.OnEvent("Click", (*) => ExportAll(window_list)) + auto_refresh_btn := inspector_gui.AddButton("x+10 yp w160", "Resume Auto Refresh") + auto_refresh_btn.OnEvent("Click", (*) => ToggleAutoRefresh(window_list, &refresh_timer, &refresh_paused, auto_refresh_btn)) + inspector_gui.OnEvent("Close", (*) => HideInspector(inspector_gui, &refresh_timer)) inspector_gui.Show() RefreshList(window_list) - StartAutoRefresh(window_list, &refresh_timer) + UpdateAutoRefreshButton(auto_refresh_btn, refresh_paused) + if !refresh_paused + StartAutoRefresh(window_list, &refresh_timer) } RefreshList(window_list) { @@ -72,6 +81,23 @@ HideInspector(inspector_gui, &refresh_timer) { inspector_gui.Hide() } +ToggleAutoRefresh(window_list, &refresh_timer, &refresh_paused, auto_refresh_btn) { + refresh_paused := !refresh_paused + if refresh_paused { + if refresh_timer + SetTimer(refresh_timer, 0) + } else { + StartAutoRefresh(window_list, &refresh_timer) + } + UpdateAutoRefreshButton(auto_refresh_btn, refresh_paused) +} + +UpdateAutoRefreshButton(auto_refresh_btn, refresh_paused) { + if !auto_refresh_btn + return + auto_refresh_btn.Text := refresh_paused ? "Resume Auto Refresh" : "Pause Auto Refresh" +} + CopySelected(window_list) { rows := [] row := 0 From 572c0fc98dc4df5219651e6f519452a510183bf4 Mon Sep 17 00:00:00 2001 From: Grant Kilber Date: Tue, 17 Feb 2026 15:34:16 -0600 Subject: [PATCH 10/20] window manager: process tree inspection for finer tuned app configs (ex: terminal applications which don't play nice with window title maintenance) --- README.md | 20 ++- config/config.example.toml | 12 ++ harken.ahk | 347 +++++++++++++++++++++++++++++++++++++ src/lib/config_loader.ahk | 45 ++++- src/lib/focus_or_run.ahk | 17 +- 5 files changed, 436 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1cdddc9..613ce25 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,8 @@ Enter Command Mode with `super + ;`. ## Window Matching App entries under `apps` can match windows via `win_title` or a `match` map. The `match` map accepts -`exe`, `class`, and `title`, plus `*_regex = true` to treat the value as a regex. +`exe`, `class`, and `title`, plus `*_regex = true` to treat the value as a regex. You can also match +by process tree with `match.process_tree`, which checks for ancestor/descendant executables. Use `exclude_titles` to ignore specific window titles for a given app. Each entry is a regex pattern and the match is case-insensitive unless you include your own `(?i)` prefix. @@ -117,6 +118,23 @@ match = { exe = "Code.exe", title = " - Visual Studio Code$", title_regex = true exclude_titles = ["^Settings$", "^Welcome$"] ``` +Process tree matching can split apps that share the same window executable: + +```toml +[[apps]] +id = "terminal" +hotkey = "s" +match = { exe = "alacritty.exe", process_tree = { mode = "descendant", exe = ["yazi.exe"], negate = true } } + +[[apps]] +id = "yazi" +hotkey = "y" +match = { exe = "alacritty.exe", process_tree = { mode = "descendant", exe = ["yazi.exe"] } } +``` + +Add `debug = true` under `match.process_tree` to log process tree details to +`%APPDATA%\harken\process_tree.debug.log`. + ### All default keybindings #### Window management (Super) diff --git a/config/config.example.toml b/config/config.example.toml index b26c1ce..618873b 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -59,6 +59,18 @@ match = { exe = "Code.exe", title = " - Visual Studio Code$", title_regex = true # Optional per-app focus border tweaks. focus_border = { border_color = "#4D96FF", border_thickness = 3, corner_radius = 12 } +# Process tree matching (example): split a terminal app and its child tool. +# +# [[apps]] +# id = "terminal" +# hotkey = "s" +# match = { exe = "alacritty.exe", process_tree = { mode = "descendant", exe = ["yazi.exe"], negate = true, debug = true } } +# +# [[apps]] +# id = "yazi" +# hotkey = "y" +# match = { exe = "alacritty.exe", process_tree = { mode = "descendant", exe = ["yazi.exe"], debug = true } } + [[global_hotkeys]] enabled = true hotkey = "Alt" diff --git a/harken.ahk b/harken.ahk index 836c7cc..6c48537 100644 --- a/harken.ahk +++ b/harken.ahk @@ -521,6 +521,11 @@ MatchWindowFields(match, hwnd) { if !MatchField(match, "title", title) return false + if match.Has("process_tree") && (match["process_tree"] is Map) { + if !MatchProcessTree(match["process_tree"], hwnd) + return false + } + return true } @@ -541,6 +546,348 @@ MatchField(match, field_key, value) { return StrLower(value) = StrLower(pattern) } +MatchProcessTree(process_tree, hwnd) { + if !(process_tree is Map) + return true + if !WinExist("ahk_id " hwnd) + return false + + exe_list := [] + if process_tree.Has("exe") { + exe_value := process_tree["exe"] + if (exe_value is Array) + exe_list := exe_value + else if (exe_value is String && exe_value != "") + exe_list := [exe_value] + } + if (exe_list.Length = 0) + return false + + mode := process_tree.Has("mode") && process_tree["mode"] != "" ? process_tree["mode"] : "descendant" + max_depth := process_tree.Has("max_depth") ? process_tree["max_depth"] : 0 + use_regex := process_tree.Has("exe_regex") && process_tree["exe_regex"] + negate := process_tree.Has("negate") && process_tree["negate"] + debug_enabled := process_tree.Has("debug") && process_tree["debug"] + + pid := 0 + try pid := WinGetPID("ahk_id " hwnd) + catch + pid := 0 + if (pid = 0) + return false + + matched := "" + context := GetMatchRunContext() + if (context is Map) { + signature := BuildProcessTreeSignature(mode, use_regex, max_depth, negate, exe_list) + memo_key := pid "|" signature + if context["memo"].Has(memo_key) + matched := context["memo"][memo_key] + } + + if (matched = "") { + matched := ProcessTreeHasExe(pid, mode, exe_list, use_regex, max_depth) + if (context is Map) + context["memo"][memo_key] := matched + } + if debug_enabled + LogProcessTreeDebug(hwnd, pid, mode, exe_list, use_regex, max_depth, negate, matched) + return negate ? !matched : matched +} + +ProcessTreeHasExe(pid, mode, exe_list, use_regex, max_depth := 0) { + snapshot := GetProcessSnapshot() + parent_map := snapshot["parent_map"] + exe_map := snapshot["exe_map"] + children_map := snapshot["children_map"] + ; Guardrail to keep descendant scans from stalling on huge trees. + max_nodes := 1000 + + if (mode = "ancestor") + return ProcessTreeMatchAncestors(pid, parent_map, exe_map, exe_list, use_regex, max_depth) + if (mode = "descendant") + return ProcessTreeMatchDescendants(pid, children_map, exe_map, exe_list, use_regex, max_depth, max_nodes) + if (mode = "either") { + if ProcessTreeMatchAncestors(pid, parent_map, exe_map, exe_list, use_regex, max_depth) + return true + return ProcessTreeMatchDescendants(pid, children_map, exe_map, exe_list, use_regex, max_depth, max_nodes) + } + + return false +} + +ProcessTreeMatchAncestors(pid, parent_map, exe_map, exe_list, use_regex, max_depth := 0) { + depth := 0 + current := pid + visited := Map() + while parent_map.Has(current) { + parent := parent_map[current] + if (parent = 0) + break + if visited.Has(parent) + break + visited[parent] := true + depth += 1 + if (max_depth > 0 && depth > max_depth) + break + if ProcessExeMatches(parent, exe_map, exe_list, use_regex) + return true + current := parent + } + return false +} + +ProcessTreeMatchDescendants(pid, children_map, exe_map, exe_list, use_regex, max_depth := 0, max_nodes := 0) { + queue := [] + depth_map := Map() + queue.Push(pid) + depth_map[pid] := 0 + scanned := 0 + + while queue.Length { + current := queue.RemoveAt(1) + depth := depth_map[current] + if (max_depth > 0 && depth >= max_depth) + continue + if !children_map.Has(current) + continue + for _, child in children_map[current] { + scanned += 1 + if (max_nodes > 0 && scanned > max_nodes) + return false + if ProcessExeMatches(child, exe_map, exe_list, use_regex) + return true + if !depth_map.Has(child) { + depth_map[child] := depth + 1 + queue.Push(child) + } + } + } + + return false +} + +ProcessExeMatches(pid, exe_map, exe_list, use_regex) { + if !exe_map.Has(pid) + return false + exe_name := exe_map[pid] + if (exe_name = "") + return false + for _, pattern in exe_list { + if (pattern = "") + continue + if use_regex { + if !RegExMatch(pattern, "^\(\?i\)") + pattern := "(?i)" pattern + if RegExMatch(exe_name, pattern) + return true + } else if (StrLower(exe_name) = StrLower(pattern)) { + return true + } + } + return false +} + +GetProcessSnapshot() { + ; Cache per match run to avoid repeated toolhelp scans. + context := GetMatchRunContext() + if (context is Map && (context["snapshot"] is Map)) + return context["snapshot"] + + ; Reuse snapshots briefly across hotkey runs to reduce lag. + static cache_tick := 0 + static cache_snapshot := Map() + now := A_TickCount + if (cache_tick && now - cache_tick < 500) + return cache_snapshot + + snapshot := BuildProcessSnapshot() + if (context is Map) + context["snapshot"] := snapshot + cache_tick := now + cache_snapshot := snapshot + return snapshot +} + +BuildProcessSnapshot() { + parent_map := Map() + exe_map := Map() + children_map := Map() + + snap := DllCall("CreateToolhelp32Snapshot", "UInt", 0x00000002, "UInt", 0, "Ptr") + if (snap = -1) + return Map("parent_map", parent_map, "exe_map", exe_map, "children_map", children_map) + + size := (A_PtrSize = 8) ? 568 : 556 + entry := Buffer(size, 0) + NumPut("UInt", size, entry, 0) + offset_pid := 8 + offset_parent := (A_PtrSize = 8) ? 32 : 24 + offset_exe := (A_PtrSize = 8) ? 44 : 36 + + if DllCall("Process32FirstW", "Ptr", snap, "Ptr", entry) { + loop { + pid := NumGet(entry, offset_pid, "UInt") + ppid := NumGet(entry, offset_parent, "UInt") + exe_name := StrGet(entry.Ptr + offset_exe, 260, "UTF-16") + if (pid != 0) { + parent_map[pid] := ppid + exe_map[pid] := exe_name + if !children_map.Has(ppid) + children_map[ppid] := [] + children_map[ppid].Push(pid) + } + if !DllCall("Process32NextW", "Ptr", snap, "Ptr", entry) + break + } + } + + DllCall("CloseHandle", "Ptr", snap) + return Map("parent_map", parent_map, "exe_map", exe_map, "children_map", children_map) +} + +BeginMatchRun() { + global process_tree_match_run, process_tree_match_run_depth + ; Scope expensive process snapshot + memo cache to a single hotkey run. + if !IsSet(process_tree_match_run_depth) + process_tree_match_run_depth := 0 + process_tree_match_run_depth += 1 + if (process_tree_match_run_depth = 1) { + process_tree_match_run := Map( + "snapshot", "", + "memo", Map() + ) + } +} + +EndMatchRun() { + global process_tree_match_run, process_tree_match_run_depth + if !IsSet(process_tree_match_run_depth) + process_tree_match_run_depth := 0 + process_tree_match_run_depth := Max(process_tree_match_run_depth - 1, 0) + if (process_tree_match_run_depth = 0) + process_tree_match_run := "" +} + +GetMatchRunContext() { + global process_tree_match_run, process_tree_match_run_depth + if !IsSet(process_tree_match_run_depth) || process_tree_match_run_depth = 0 + return "" + if !(process_tree_match_run is Map) + return "" + return process_tree_match_run +} + +BuildProcessTreeSignature(mode, use_regex, max_depth, negate, exe_list) { + return mode "|" use_regex "|" max_depth "|" negate "|" StrJoin(exe_list, ",") +} + +LogProcessTreeDebug(hwnd, pid, mode, exe_list, use_regex, max_depth, negate, matched) { + log_dir := GetAppDataDir() + DirCreate(log_dir) + log_path := log_dir "\\process_tree.debug.log" + + lines := [] + lines.Push("[" A_Now "] hwnd=" Format("0x{:X}", hwnd) " pid=" pid) + lines.Push(" mode=" mode " use_regex=" use_regex " max_depth=" max_depth " negate=" negate " matched=" matched) + lines.Push(" exe_list=" StrJoin(exe_list, ",")) + + snapshot := GetProcessSnapshot() + parent_map := snapshot["parent_map"] + exe_map := snapshot["exe_map"] + children_map := snapshot["children_map"] + + lines.Push(" ancestors:") + for _, item in BuildProcessTreeAncestorSummary(pid, parent_map, exe_map, max_depth) + lines.Push(" " item) + + lines.Push(" descendants:") + for _, item in BuildProcessTreeDescendantSummary(pid, children_map, exe_map, max_depth) + lines.Push(" " item) + + SafeFileAppend(StrJoin(lines, "`n") "`n", log_path) +} + +BuildProcessTreeAncestorSummary(pid, parent_map, exe_map, max_depth := 0) { + max_rows := 20 + rows := [] + depth := 0 + current := pid + visited := Map() + while parent_map.Has(current) { + parent := parent_map[current] + if (parent = 0) + break + if visited.Has(parent) + break + visited[parent] := true + depth += 1 + if (max_depth > 0 && depth > max_depth) + break + exe_name := exe_map.Has(parent) ? exe_map[parent] : "" + rows.Push("d" depth " pid=" parent " exe=" exe_name) + if (rows.Length >= max_rows) + break + current := parent + } + if (rows.Length = 0) + rows.Push("(none)") + return rows +} + +BuildProcessTreeDescendantSummary(pid, children_map, exe_map, max_depth := 0) { + max_rows := 40 + rows := [] + queue := [] + depth_map := Map() + queue.Push(pid) + depth_map[pid] := 0 + + while queue.Length { + current := queue.RemoveAt(1) + depth := depth_map[current] + if (max_depth > 0 && depth >= max_depth) + continue + if !children_map.Has(current) + continue + for _, child in children_map[current] { + exe_name := exe_map.Has(child) ? exe_map[child] : "" + rows.Push("d" (depth + 1) " pid=" child " exe=" exe_name) + if (rows.Length >= max_rows) + return rows + if !depth_map.Has(child) { + depth_map[child] := depth + 1 + queue.Push(child) + } + } + } + + if (rows.Length = 0) + rows.Push("(none)") + return rows +} + +SafeFileAppend(text, path, retries := 3) { + ; Guard against re-entrancy and transient file locks. + static writing := false + if writing + return false + writing := true + attempt := 0 + loop retries { + attempt += 1 + try { + FileAppend(text, path) + writing := false + return true + } catch { + Sleep(20) + } + } + writing := false + return false +} + OpenWindowInspector() { ShowWindowInspector() } diff --git a/src/lib/config_loader.ahk b/src/lib/config_loader.ahk index e72a23a..8b4826f 100644 --- a/src/lib/config_loader.ahk +++ b/src/lib/config_loader.ahk @@ -47,7 +47,15 @@ ConfigSchema() { "class", OptionalSpec("string"), "class_regex", OptionalSpec("bool"), "title", OptionalSpec("string"), - "title_regex", OptionalSpec("bool") + "title_regex", OptionalSpec("bool"), + "process_tree", OptionalSpec(Map( + "mode", OptionalSpec("string"), + "exe", OptionalSpec(["string"]), + "exe_regex", OptionalSpec("bool"), + "max_depth", OptionalSpec("number"), + "negate", OptionalSpec("bool"), + "debug", OptionalSpec("bool") + )) )), "run", OptionalSpec("string"), "run_start_in", OptionalSpec("string"), @@ -206,12 +214,28 @@ NormalizeAppsConfig(config, errors) { app_config.Delete("id") } app_config["id"] := app_id + NormalizeAppProcessTreeConfig(app_config) normalized.Push(app_config) } config["apps"] := normalized } +NormalizeAppProcessTreeConfig(app_config) { + if !(app_config is Map) + return + if !app_config.Has("match") || !(app_config["match"] is Map) + return + match := app_config["match"] + if !match.Has("process_tree") || !(match["process_tree"] is Map) + return + process_tree := match["process_tree"] + if process_tree.Has("exe") && (process_tree["exe"] is String) + process_tree["exe"] := [process_tree["exe"]] + if !process_tree.Has("mode") || process_tree["mode"] = "" + process_tree["mode"] := "descendant" +} + NormalizeVirtualDesktopConfig(config) { if !config.Has("virtual_desktop") || !(config["virtual_desktop"] is Map) @@ -302,8 +326,9 @@ ValidateApps(config, errors) { has_exe := match.Has("exe") && (match["exe"] != "") has_class := match.Has("class") && (match["class"] != "") has_title := match.Has("title") && (match["title"] != "") - if !has_exe && !has_class && !has_title - errors.Push("config.apps[" index "].match must define exe, class, or title") + has_process_tree := match.Has("process_tree") && (match["process_tree"] is Map) + if !has_exe && !has_class && !has_title && !has_process_tree + errors.Push("config.apps[" index "].match must define exe, class, title, or process_tree") if (match.Has("exe_regex") && !has_exe) errors.Push("config.apps[" index "].match.exe_regex requires exe") @@ -311,6 +336,20 @@ ValidateApps(config, errors) { errors.Push("config.apps[" index "].match.class_regex requires class") if (match.Has("title_regex") && !has_title) errors.Push("config.apps[" index "].match.title_regex requires title") + + if has_process_tree { + process_tree := match["process_tree"] + has_tree_exe := process_tree.Has("exe") && (process_tree["exe"] is Array) && process_tree["exe"].Length + if !has_tree_exe + errors.Push("config.apps[" index "].match.process_tree.exe must define one or more executables") + if process_tree.Has("exe_regex") && !has_tree_exe + errors.Push("config.apps[" index "].match.process_tree.exe_regex requires exe") + mode := process_tree.Has("mode") ? process_tree["mode"] : "descendant" + if (mode != "ancestor" && mode != "descendant" && mode != "either") + errors.Push("config.apps[" index "].match.process_tree.mode must be ancestor, descendant, or either") + if process_tree.Has("max_depth") && (process_tree["max_depth"] < 0) + errors.Push("config.apps[" index "].match.process_tree.max_depth must be >= 0") + } } } } diff --git a/src/lib/focus_or_run.ahk b/src/lib/focus_or_run.ahk index a87b07d..31a616e 100644 --- a/src/lib/focus_or_run.ahk +++ b/src/lib/focus_or_run.ahk @@ -5,7 +5,9 @@ FocusOrRun(winTitle, exePath, hotkey_id, app_config := "", *) { static last_window := Map() target_hwnd := 0 + BeginMatchRun() hwnds := GetAppWindowList(winTitle, app_config) + EndMatchRun() current_hwnd := 0 try current_hwnd := WinGetID("A") catch @@ -45,13 +47,26 @@ GetAppWindowList(win_title, app_config := "") { GetWindowsByMatch(match, app_config := "") { matches := [] - for _, hwnd in GetWindowsAcrossDesktops() { + win_list := GetWindowsByMatchCandidates(match) + for _, hwnd in win_list { if MatchWindowFields(match, hwnd) && !AppConfigExcludesTitle(app_config, hwnd) matches.Push(hwnd) } return matches } +GetWindowsByMatchCandidates(match) { + if !(match is Map) + return GetWindowsAcrossDesktops() + if match.Has("exe") && match["exe"] != "" && !(match.Has("exe_regex") && match["exe_regex"]) { + exe_name := match["exe"] + if RegExMatch(exe_name, "i)^ahk_exe\s+") + return GetWindowsAcrossDesktops(exe_name) + return GetWindowsAcrossDesktops("ahk_exe " exe_name) + } + return GetWindowsAcrossDesktops() +} + FilterExcludedWindows(hwnds, app_config := "") { if !(app_config is Map) return hwnds From 8fbefe05404128298814c430774774b884501d14 Mon Sep 17 00:00:00 2001 From: Grant Kilber Date: Wed, 18 Feb 2026 16:21:52 -0600 Subject: [PATCH 11/20] Add process-tree matching optimizations and focus diagnostics Refine app matching to use cached window metadata and PID-based lookups for off-desktop windows, plus debug logging for focus/match issues to track why targets are missed. --- README.md | 1 + config/config.example.toml | 1 + harken.ahk | 266 +++++++++++++++++++++- src/hotkeys/window.ahk | 4 +- src/lib/config_loader.ahk | 1 + src/lib/focus_or_run.ahk | 443 +++++++++++++++++++++++++++++++++++- src/lib/virtual_desktop.ahk | 115 +++++++++- 7 files changed, 800 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 613ce25..1808a62 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,7 @@ Add `debug = true` under `match.process_tree` to log process tree details to | `super + alt + ` | Go to mapped desktop (`[[virtual_desktop.]]`) | | `super + alt + shift + ` | Move window to mapped desktop (follow) | | `virtual_desktop.auto_assign` | Move newly created windows that match `apps[]` with `desktop` set | +| `virtual_desktop.debug_focus` | Log cross-desktop focus attempts to `%APPDATA%\harken\vd.focus.debug.log` | #### Apps (defaults) diff --git a/config/config.example.toml b/config/config.example.toml index 618873b..808daf6 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -129,6 +129,7 @@ move_next_hotkey = "l" # Debug logs write to %APPDATA%\harken when enabled. debug_cycle = false debug_hotkeys = false +# debug_focus = false tray_indicator = true tray_format = "{current}/{total}" # auto_assign = false diff --git a/harken.ahk b/harken.ahk index 6c48537..b361887 100644 --- a/harken.ahk +++ b/harken.ahk @@ -140,6 +140,7 @@ DefaultConfig() { "move_hotkeys", [], "debug_cycle", false, "debug_hotkeys", false, + "debug_focus", false, "tray_indicator", false, "tray_format", "{current}/{total}", "auto_assign", false, @@ -452,6 +453,9 @@ RegisterSuperComboHotkey(hotkey_name, callback) { Hotkey(key " & " hotkey_name, callback) } +global WINDOW_MATCH_CACHE_TTL_MS := 300000 +global window_match_cache := Map() + MatchAppWindow(app, hwnd := 0) { if !(app is Map) return false @@ -501,18 +505,61 @@ AppConfigExcludesTitle(app, hwnd) { } MatchWindowFields(match, hwnd) { - if !WinExist("ahk_id " hwnd) + if !WindowExistsAcrossDesktops(hwnd) return false ; Window handles can go stale during app switching; guard WinGet* calls. - try exe_name := WinGetProcessName("ahk_id " hwnd) - catch - return false - try class_name := WinGetClass("ahk_id " hwnd) - catch - return false - try title := WinGetTitle("ahk_id " hwnd) - catch - return false + bak_detect_hidden_windows := A_DetectHiddenWindows + A_DetectHiddenWindows := true + exe_name := "" + class_name := "" + title := "" + pid := 0 + got_any := false + try { + exe_name := WinGetProcessName("ahk_id " hwnd) + got_any := true + } + try { + class_name := WinGetClass("ahk_id " hwnd) + got_any := true + } + try { + title := WinGetTitle("ahk_id " hwnd) + got_any := true + } + try { + pid := WinGetPID("ahk_id " hwnd) + got_any := true + } + A_DetectHiddenWindows := bak_detect_hidden_windows + if (pid = 0) + pid := GetWindowPidForMatch(hwnd) + + if (!got_any) { + cached := GetWindowMatchCache(hwnd) + if (cached = "") + return false + } else { + cached := GetWindowMatchCache(hwnd) + } + + if (exe_name = "" && cached) + exe_name := cached["exe"] + if (exe_name = "" && pid) + exe_name := GetProcessNameFromPid(pid) + if (exe_name = "" && pid) + exe_name := GetProcessNameFromSnapshot(pid) + if (class_name = "" && cached) + class_name := cached["class"] + if (title = "" && cached) + title := cached["title"] + if (pid = 0 && cached) + pid := cached["pid"] + if (pid = 0) + pid := GetWindowPidForMatch(hwnd) + + if (got_any) + SetWindowMatchCache(hwnd, exe_name, class_name, title, pid) if !MatchField(match, "exe", exe_name) return false @@ -573,6 +620,11 @@ MatchProcessTree(process_tree, hwnd) { try pid := WinGetPID("ahk_id " hwnd) catch pid := 0 + if (pid = 0) { + cached := GetWindowMatchCache(hwnd) + if (cached) + pid := cached["pid"] + } if (pid = 0) return false @@ -595,6 +647,51 @@ MatchProcessTree(process_tree, hwnd) { return negate ? !matched : matched } +GetProcessTreeMatchDetail(process_tree, hwnd) { + detail := Map("enabled", false) + if !(process_tree is Map) + return detail + if !WindowExistsAcrossDesktops(hwnd) + return detail + + exe_list := [] + if process_tree.Has("exe") { + exe_value := process_tree["exe"] + if (exe_value is Array) + exe_list := exe_value + else if (exe_value is String && exe_value != "") + exe_list := [exe_value] + } + if (exe_list.Length = 0) + return detail + + mode := process_tree.Has("mode") && process_tree["mode"] != "" ? process_tree["mode"] : "descendant" + max_depth := process_tree.Has("max_depth") ? process_tree["max_depth"] : 0 + use_regex := process_tree.Has("exe_regex") && process_tree["exe_regex"] + negate := process_tree.Has("negate") && process_tree["negate"] + + bak_detect_hidden_windows := A_DetectHiddenWindows + A_DetectHiddenWindows := true + pid := 0 + try pid := WinGetPID("ahk_id " hwnd) + catch + pid := 0 + A_DetectHiddenWindows := bak_detect_hidden_windows + if (pid = 0) + pid := GetWindowPidForMatch(hwnd) + if (pid = 0) + return detail + + matched := ProcessTreeHasExe(pid, mode, exe_list, use_regex, max_depth) + detail["enabled"] := true + detail["matched"] := matched + detail["final"] := negate ? !matched : matched + detail["negate"] := negate + detail["mode"] := mode + detail["max_depth"] := max_depth + return detail +} + ProcessTreeHasExe(pid, mode, exe_list, use_regex, max_depth := 0) { snapshot := GetProcessSnapshot() parent_map := snapshot["parent_map"] @@ -782,6 +879,155 @@ BuildProcessTreeSignature(mode, use_regex, max_depth, negate, exe_list) { return mode "|" use_regex "|" max_depth "|" negate "|" StrJoin(exe_list, ",") } +GetWindowMatchCache(hwnd) { + global window_match_cache, WINDOW_MATCH_CACHE_TTL_MS + if !IsSet(WINDOW_MATCH_CACHE_TTL_MS) + WINDOW_MATCH_CACHE_TTL_MS := 300000 + if !IsSet(window_match_cache) || !(window_match_cache is Map) + window_match_cache := Map() + if !window_match_cache.Has(hwnd) + return "" + entry := window_match_cache[hwnd] + if !(entry is Map) { + window_match_cache.Delete(hwnd) + return "" + } + if !entry.Has("seen_at") || (A_TickCount - entry["seen_at"] > WINDOW_MATCH_CACHE_TTL_MS) { + window_match_cache.Delete(hwnd) + return "" + } + return entry +} + +SetWindowMatchCache(hwnd, exe_name, class_name, title, pid := 0) { + global window_match_cache + if !IsSet(window_match_cache) || !(window_match_cache is Map) + window_match_cache := Map() + entry := window_match_cache.Has(hwnd) ? window_match_cache[hwnd] : Map() + entry["exe"] := exe_name + entry["class"] := class_name + entry["title"] := title + entry["pid"] := pid + entry["seen_at"] := A_TickCount + window_match_cache[hwnd] := entry +} + +TryUpdateWindowMatchCache(hwnd) { + exe_name := "" + class_name := "" + title := "" + pid := 0 + got_any := false + bak_detect_hidden_windows := A_DetectHiddenWindows + A_DetectHiddenWindows := true + if VirtualDesktopEnabled() { + desktop_num := GetWindowDesktopNum(hwnd) + if (desktop_num <= 0) + return + } + try { + exe_name := WinGetProcessName("ahk_id " hwnd) + got_any := true + } + try { + class_name := WinGetClass("ahk_id " hwnd) + got_any := true + } + try { + title := WinGetTitle("ahk_id " hwnd) + got_any := true + } + try { + pid := WinGetPID("ahk_id " hwnd) + got_any := true + } + A_DetectHiddenWindows := bak_detect_hidden_windows + if (pid = 0) + pid := GetWindowPidForMatch(hwnd) + if (exe_name = "" && pid) + exe_name := GetProcessNameFromPid(pid) + if (exe_name = "" && pid) + exe_name := GetProcessNameFromSnapshot(pid) + if !got_any + return + + global window_match_cache + if !IsSet(window_match_cache) || !(window_match_cache is Map) + window_match_cache := Map() + cached := window_match_cache.Has(hwnd) ? window_match_cache[hwnd] : "" + if (exe_name = "" && cached) + exe_name := cached["exe"] + if (class_name = "" && cached) + class_name := cached["class"] + if (title = "" && cached) + title := cached["title"] + if (pid = 0 && cached) + pid := cached["pid"] + + SetWindowMatchCache(hwnd, exe_name, class_name, title, pid) +} + +UpdateWindowMatchCacheFromList(hwnds) { + if !(hwnds is Array) + return + for _, hwnd in hwnds + TryUpdateWindowMatchCache(hwnd) +} + +GetCachedWindowsByExe(exe_name) { + global window_match_cache + if !IsSet(window_match_cache) || !(window_match_cache is Map) + window_match_cache := Map() + list := [] + if (exe_name = "") + return list + target := StrLower(exe_name) + for hwnd, entry in window_match_cache { + if !WindowExistsAcrossDesktops(hwnd) { + window_match_cache.Delete(hwnd) + continue + } + if VirtualDesktopEnabled() { + desktop_num := GetWindowDesktopNum(hwnd) + if (desktop_num <= 0) + continue + } + cached := GetWindowMatchCache(hwnd) + if (cached = "") + continue + exe_value := cached.Has("exe") ? cached["exe"] : "" + if (exe_value = "") + continue + if (StrLower(exe_value) = target) + list.Push(hwnd) + } + return list +} + +GetProcessNameFromPid(pid) { + if (pid <= 0) + return "" + try return ProcessGetName(pid) + catch + return "" +} + +GetProcessNameFromSnapshot(pid) { + if (pid <= 0) + return "" + snapshot := GetProcessSnapshot() + exe_map := snapshot["exe_map"] + if exe_map.Has(pid) + return exe_map[pid] + return "" +} + +GetWindowPidForMatch(hwnd) { + pid := 0 + DllCall("GetWindowThreadProcessId", "Ptr", hwnd, "UInt*", &pid) + return pid +} + LogProcessTreeDebug(hwnd, pid, mode, exe_list, use_regex, max_depth, negate, matched) { log_dir := GetAppDataDir() DirCreate(log_dir) diff --git a/src/hotkeys/window.ahk b/src/hotkeys/window.ahk index c7964fc..60fa713 100644 --- a/src/hotkeys/window.ahk +++ b/src/hotkeys/window.ahk @@ -199,8 +199,8 @@ FilterWindowList(exe, list) { if VirtualDesktopEnabled() { desktop_num := GetWindowDesktopNum(id) if (desktop_num <= 0) - allow_invisible := true - else if (desktop_num != VD.getCurrentDesktopNum()) + continue + if (desktop_num > 0 && desktop_num != VD.getCurrentDesktopNum()) allow_invisible := true } if (!allow_invisible && !(style & 0x10000000)) diff --git a/src/lib/config_loader.ahk b/src/lib/config_loader.ahk index 8b4826f..4f5b848 100644 --- a/src/lib/config_loader.ahk +++ b/src/lib/config_loader.ahk @@ -136,6 +136,7 @@ ConfigSchema() { )], "debug_cycle", "bool", "debug_hotkeys", "bool", + "debug_focus", OptionalSpec("bool"), "tray_indicator", "bool", "tray_format", "string", "auto_assign", "bool", diff --git a/src/lib/focus_or_run.ahk b/src/lib/focus_or_run.ahk index 31a616e..10b4e8c 100644 --- a/src/lib/focus_or_run.ahk +++ b/src/lib/focus_or_run.ahk @@ -8,6 +8,8 @@ FocusOrRun(winTitle, exePath, hotkey_id, app_config := "", *) { BeginMatchRun() hwnds := GetAppWindowList(winTitle, app_config) EndMatchRun() + if VirtualDesktopFocusDebugEnabled() + LogFocusOrRunCandidates(hotkey_id, app_config, hwnds) current_hwnd := 0 try current_hwnd := WinGetID("A") catch @@ -16,10 +18,12 @@ FocusOrRun(winTitle, exePath, hotkey_id, app_config := "", *) { if (hwnds.Length) { target_hwnd := PickFocusableAppWindow(hwnds, winTitle) if (!target_hwnd) - target_hwnd := hwnds[1] + target_hwnd := PickDesktopAssignedWindow(hwnds) } if target_hwnd { + if VirtualDesktopFocusDebugEnabled() + LogFocusOrRunSelection(hotkey_id, app_config, target_hwnd) if (current_hwnd = target_hwnd) { if last_window.Has(hotkey_id) && WindowExistsAcrossDesktops(last_window[hotkey_id]) { ActivateWindowAcrossDesktops(last_window[hotkey_id]) @@ -31,6 +35,8 @@ FocusOrRun(winTitle, exePath, hotkey_id, app_config := "", *) { } ActivateWindowAcrossDesktops(target_hwnd) } else { + if VirtualDesktopFocusDebugEnabled() + LogFocusOrRunMiss(hotkey_id, app_config) RunResolved(exePath, app_config) ScheduleMoveAppWindowToDesktop(winTitle, app_config) } @@ -42,16 +48,48 @@ GetAppWindowList(win_title, app_config := "") { } if !win_title return [] - return FilterExcludedWindows(GetWindowsAcrossDesktops(win_title), app_config) + win_list := GetWindowsAcrossDesktops(win_title) + if RegExMatch(win_title, "i)^ahk_exe\s+") { + exe_name := RegExReplace(win_title, "i)^ahk_exe\s+", "") + win_list := MergeWindowLists(win_list, CollectWindowsByExeFallback(exe_name, win_list)) + } + win_list := FilterDesktopAssignedWindows(win_list, true) + UpdateWindowMatchCacheFromList(win_list) + return FilterExcludedWindows(win_list, app_config) } GetWindowsByMatch(match, app_config := "") { matches := [] + match_true := 0 + exclude_true := 0 + debug_results := [] win_list := GetWindowsByMatchCandidates(match) + win_list := FilterDesktopAssignedWindows(win_list, true) + UpdateWindowMatchCacheFromList(win_list) + bak_detect_hidden_windows := A_DetectHiddenWindows + A_DetectHiddenWindows := true for _, hwnd in win_list { - if MatchWindowFields(match, hwnd) && !AppConfigExcludesTitle(app_config, hwnd) + matched := MatchWindowFields(match, hwnd) + if matched + match_true += 1 + excluded := AppConfigExcludesTitle(app_config, hwnd) + if excluded + exclude_true += 1 + if VirtualDesktopFocusDebugEnabled() { + debug_results.Push(Map( + "hwnd", hwnd, + "matched", matched, + "excluded", excluded + )) + } + if matched && !excluded matches.Push(hwnd) } + A_DetectHiddenWindows := bak_detect_hidden_windows + if VirtualDesktopFocusDebugEnabled() && matches.Length = 0 { + LogMatchDiagnostics(match, win_list, app_config, debug_results) + LogMatchCounts(match_true, exclude_true, win_list) + } return matches } @@ -61,12 +99,368 @@ GetWindowsByMatchCandidates(match) { if match.Has("exe") && match["exe"] != "" && !(match.Has("exe_regex") && match["exe_regex"]) { exe_name := match["exe"] if RegExMatch(exe_name, "i)^ahk_exe\s+") - return GetWindowsAcrossDesktops(exe_name) - return GetWindowsAcrossDesktops("ahk_exe " exe_name) + exe_name := RegExReplace(exe_name, "i)^ahk_exe\s+", "") + win_list := GetWindowsAcrossDesktops("ahk_exe " exe_name) + return MergeWindowLists(win_list, CollectWindowsByExeFallback(exe_name, win_list)) } return GetWindowsAcrossDesktops() } +MergeWindowLists(primary, fallback) { + if !(fallback is Array) + return primary + if !(primary is Array) + primary := [] + if (fallback.Length = 0) + return primary + dedup := Map() + merged := [] + for _, hwnd in primary { + dedup[hwnd] := true + merged.Push(hwnd) + } + for _, hwnd in fallback { + if !dedup.Has(hwnd) { + merged.Push(hwnd) + dedup[hwnd] := true + } + } + return merged +} + +FilterDesktopAssignedWindows(hwnds, allow_fallback := false) { + if !VirtualDesktopEnabled() + return hwnds + filtered := [] + for _, hwnd in hwnds { + desktop_num := GetWindowDesktopNum(hwnd) + if (desktop_num <= 0) + continue + filtered.Push(hwnd) + } + if (allow_fallback && filtered.Length = 0) + return hwnds + return filtered +} + +CollectWindowsByExeFallback(exe_name, win_list) { + merged := [] + cached := GetCachedWindowsByExe(exe_name) + merged := MergeWindowLists(merged, cached) + expanded := [] + if ShouldExpandExeSearch(win_list) + expanded := CollectWindowsByExe(exe_name) + merged := MergeWindowLists(merged, expanded) + if VirtualDesktopFocusDebugEnabled() + LogFocusOrRunExeFallback(exe_name, win_list, cached, expanded, merged) + return merged +} + +ShouldExpandExeSearch(win_list) { + if !VirtualDesktopEnabled() + return false + if !(win_list is Array) || win_list.Length = 0 + return true + has_valid := false + has_invalid := false + for _, hwnd in win_list { + desktop_num := GetWindowDesktopNum(hwnd) + if (desktop_num > 0) + has_valid := true + else + has_invalid := true + } + if VirtualDesktopFocusDebugEnabled() + LogExeExpandDecision(win_list.Length, has_valid, has_invalid) + return !has_valid +} + +CollectWindowsByExe(exe_name) { + if (exe_name = "") + return [] + filtered := [] + ; WinGetList may miss off-desktop windows for some apps. + win_list := GetAllTopLevelWindows() + pid_set := BuildProcessPidSet(exe_name) + total_windows := win_list.Length + pid_zero := 0 + pid_match := 0 + pid_miss := 0 + bak_detect_hidden_windows := A_DetectHiddenWindows + A_DetectHiddenWindows := true + for _, hwnd in win_list { + if !WindowExistsAcrossDesktops(hwnd) + continue + pid := GetWindowPid(hwnd) + if (pid = 0) { + pid_zero += 1 + continue + } + if !pid_set.Has(pid) { + pid_miss += 1 + continue + } + pid_match += 1 + if VirtualDesktopEnabled() { + desktop_num := GetWindowDesktopNum(hwnd) + if (desktop_num <= 0) + continue + } + filtered.Push(hwnd) + } + A_DetectHiddenWindows := bak_detect_hidden_windows + if VirtualDesktopFocusDebugEnabled() { + LogFocusOrRunEnumStats(exe_name, total_windows, pid_set.Count, pid_match, pid_miss, pid_zero) + if (pid_set.Count = 0) + LogFocusOrRunSnapshotStats(exe_name) + } + return filtered +} + +BuildProcessPidSet(exe_name) { + pid_set := Map() + if (exe_name = "") + return pid_set + snapshot := GetProcessSnapshot() + exe_map := snapshot["exe_map"] + target := StrLower(exe_name) + for pid, name in exe_map { + if (StrLower(name) = target) + pid_set[pid] := true + } + return pid_set +} + +LogFocusOrRunEnumStats(exe_name, total_windows, pid_set_count, pid_match, pid_miss, pid_zero) { + log_dir := GetAppDataDir() + DirCreate(log_dir) + log_path := log_dir "\\vd.focus.debug.log" + line := "[" A_Now "] enum_stats exe=" exe_name + line .= " windows=" total_windows + line .= " pid_set=" pid_set_count + line .= " pid_match=" pid_match + line .= " pid_miss=" pid_miss + line .= " pid_zero=" pid_zero + SafeFileAppend(line "`n", log_path) +} + +LogFocusOrRunSnapshotStats(exe_name) { + log_dir := GetAppDataDir() + DirCreate(log_dir) + log_path := log_dir "\\vd.focus.debug.log" + snapshot := GetProcessSnapshot() + exe_map := snapshot["exe_map"] + exact := 0 + contains_count := 0 + total := 0 + target := StrLower(exe_name) + needle := StrLower(RegExReplace(exe_name, "\.exe$", "")) + for _, name in exe_map { + total += 1 + lower := StrLower(name) + if (lower = target) + exact += 1 + if (InStr(lower, needle)) + contains_count += 1 + } + line := "[" A_Now "] snapshot_stats exe=" exe_name + line .= " total=" total + line .= " exact=" exact + line .= " contains=" contains_count + SafeFileAppend(line "`n", log_path) +} + +GetAllTopLevelWindows() { + hwnds := [] + callback := CallbackCreate(EnumWindowsCallback.Bind(hwnds), "Fast") + DllCall("EnumWindows", "Ptr", callback, "Ptr", 0) + CallbackFree(callback) + return hwnds +} + +EnumWindowsCallback(hwnds, hwnd, lparam) { + hwnds.Push(hwnd) + return true +} + +GetWindowPid(hwnd) { + pid := 0 + DllCall("GetWindowThreadProcessId", "Ptr", hwnd, "UInt*", &pid) + return pid +} + +LogFocusOrRunCandidates(hotkey_id, app_config, hwnds) { + log_dir := GetAppDataDir() + DirCreate(log_dir) + log_path := log_dir "\\vd.focus.debug.log" + app_id := (app_config is Map && app_config.Has("id")) ? app_config["id"] : "" + lines := [] + lines.Push("[" A_Now "] focus_candidates hotkey=" hotkey_id " app=" app_id " count=" (hwnds is Array ? hwnds.Length : 0)) + bak_detect_hidden_windows := A_DetectHiddenWindows + A_DetectHiddenWindows := true + if (hwnds is Array) { + for _, hwnd in hwnds { + desktop_num := VirtualDesktopEnabled() ? GetWindowDesktopNum(hwnd) : 0 + exe_name := "" + class_name := "" + title := "" + exe_name := "" + try exe_name := WinGetProcessName("ahk_id " hwnd) + if (exe_name = "") { + pid := GetWindowPid(hwnd) + exe_name := GetProcessNameFromPid(pid) + if (exe_name = "") + exe_name := GetProcessNameFromSnapshot(pid) + } + try class_name := WinGetClass("ahk_id " hwnd) + try title := WinGetTitle("ahk_id " hwnd) + lines.Push(" hwnd=" Format("0x{:X}", hwnd) " desktop=" desktop_num " exe=" exe_name " class=" class_name " title=" title) + } + } + A_DetectHiddenWindows := bak_detect_hidden_windows + SafeFileAppend(StrJoin(lines, "`n") "`n", log_path) +} + +LogFocusOrRunSelection(hotkey_id, app_config, hwnd) { + log_dir := GetAppDataDir() + DirCreate(log_dir) + log_path := log_dir "\\vd.focus.debug.log" + app_id := (app_config is Map && app_config.Has("id")) ? app_config["id"] : "" + desktop_num := VirtualDesktopEnabled() ? GetWindowDesktopNum(hwnd) : 0 + exe_name := "" + class_name := "" + title := "" + try exe_name := WinGetProcessName("ahk_id " hwnd) + try class_name := WinGetClass("ahk_id " hwnd) + try title := WinGetTitle("ahk_id " hwnd) + line := "[" A_Now "] focus_select hotkey=" hotkey_id " app=" app_id + line .= " hwnd=" Format("0x{:X}", hwnd) " desktop=" desktop_num " exe=" exe_name " class=" class_name " title=" title + SafeFileAppend(line "`n", log_path) +} + +LogFocusOrRunMiss(hotkey_id, app_config) { + log_dir := GetAppDataDir() + DirCreate(log_dir) + log_path := log_dir "\\vd.focus.debug.log" + app_id := (app_config is Map && app_config.Has("id")) ? app_config["id"] : "" + SafeFileAppend("[" A_Now "] focus_miss hotkey=" hotkey_id " app=" app_id "`n", log_path) +} + +LogFocusOrRunExeFallback(exe_name, win_list, cached, expanded, merged) { + log_dir := GetAppDataDir() + DirCreate(log_dir) + log_path := log_dir "\\vd.focus.debug.log" + line := "[" A_Now "] exe_fallback exe=" exe_name + line .= " winget=" (win_list is Array ? win_list.Length : 0) + line .= " cached=" (cached is Array ? cached.Length : 0) + line .= " enum=" (expanded is Array ? expanded.Length : 0) + line .= " merged=" (merged is Array ? merged.Length : 0) + SafeFileAppend(line "`n", log_path) +} + +LogMatchDiagnostics(match, win_list, app_config := "", debug_results := "") { + log_dir := GetAppDataDir() + DirCreate(log_dir) + log_path := log_dir "\\vd.focus.debug.log" + exe_pattern := match.Has("exe") ? match["exe"] : "" + class_pattern := match.Has("class") ? match["class"] : "" + title_pattern := match.Has("title") ? match["title"] : "" + exe_regex := match.Has("exe_regex") && match["exe_regex"] + class_regex := match.Has("class_regex") && match["class_regex"] + title_regex := match.Has("title_regex") && match["title_regex"] + + lines := [] + lines.Push("[" A_Now "] match_diag exe=" exe_pattern " class=" class_pattern " title=" title_pattern) + lines.Push(" regex exe=" exe_regex " class=" class_regex " title=" title_regex " candidates=" (win_list is Array ? win_list.Length : 0)) + process_tree := match.Has("process_tree") && (match["process_tree"] is Map) ? match["process_tree"] : "" + + bak_detect_hidden_windows := A_DetectHiddenWindows + A_DetectHiddenWindows := true + sample_max := 12 + if (win_list is Array) { + debug_index := 1 + for _, hwnd in win_list { + if (sample_max <= 0) + break + sample_max -= 1 + exists := WindowExistsAcrossDesktops(hwnd) + desktop_num := VirtualDesktopEnabled() ? GetWindowDesktopNum(hwnd) : 0 + pid := GetWindowPid(hwnd) + exe_name := "" + class_name := "" + title := "" + try exe_name := WinGetProcessName("ahk_id " hwnd) + if (exe_name = "" && pid) + exe_name := GetProcessNameFromPid(pid) + if (exe_name = "" && pid) + exe_name := GetProcessNameFromSnapshot(pid) + try class_name := WinGetClass("ahk_id " hwnd) + try title := WinGetTitle("ahk_id " hwnd) + + exe_ok := MatchFieldValue(exe_pattern, exe_name, exe_regex) + class_ok := MatchFieldValue(class_pattern, class_name, class_regex) + title_ok := MatchFieldValue(title_pattern, title, title_regex) + tree_ok := "" + tree_matched := "" + if (process_tree is Map) { + detail := GetProcessTreeMatchDetail(process_tree, hwnd) + if detail["enabled"] { + tree_ok := detail["final"] + tree_matched := detail["matched"] + } + } + match_ok := MatchWindowFields(match, hwnd) + exclude_ok := "" + if (app_config is Map) + exclude_ok := AppConfigExcludesTitle(app_config, hwnd) + lines.Push(" hwnd=" Format("0x{:X}", hwnd) " desktop=" desktop_num " pid=" pid) + lines.Push(" exists=" exists " match_ok=" match_ok " exclude=" exclude_ok) + lines.Push(" exe=" exe_name " match=" exe_ok) + lines.Push(" class=" class_name " match=" class_ok) + lines.Push(" title=" title " match=" title_ok) + if (tree_ok != "") + lines.Push(" process_tree matched=" tree_matched " final=" tree_ok) + if (debug_results is Array && debug_index <= debug_results.Length) { + entry := debug_results[debug_index] + debug_index += 1 + loop_match := entry.Has("matched") ? entry["matched"] : "" + loop_exclude := entry.Has("excluded") ? entry["excluded"] : "" + lines.Push(" loop matched=" loop_match " excluded=" loop_exclude) + } + } + } + A_DetectHiddenWindows := bak_detect_hidden_windows + SafeFileAppend(StrJoin(lines, "`n") "`n", log_path) +} + +LogMatchCounts(match_true, exclude_true, win_list) { + log_dir := GetAppDataDir() + DirCreate(log_dir) + log_path := log_dir "\\vd.focus.debug.log" + total := (win_list is Array ? win_list.Length : 0) + line := "[" A_Now "] match_counts total=" total " matched=" match_true " excluded=" exclude_true + SafeFileAppend(line "`n", log_path) +} + +MatchFieldValue(pattern, value, use_regex) { + if (pattern = "") + return true + if use_regex { + if !RegExMatch(pattern, "^\(\?i\)") + pattern := "(?i)" pattern + return RegExMatch(value, pattern) != 0 + } + return StrLower(value) = StrLower(pattern) +} + +LogExeExpandDecision(count, has_valid, has_invalid) { + log_dir := GetAppDataDir() + DirCreate(log_dir) + log_path := log_dir "\\vd.focus.debug.log" + line := "[" A_Now "] exe_expand candidates=" count " has_valid=" has_valid " has_invalid=" has_invalid + SafeFileAppend(line "`n", log_path) +} + FilterExcludedWindows(hwnds, app_config := "") { if !(app_config is Map) return hwnds @@ -84,10 +478,12 @@ FilterExcludedWindows(hwnds, app_config := "") { PickFocusableAppWindow(hwnds, win_title) { for hwnd in hwnds { - if !WinExist("ahk_id " hwnd) + if !WindowExistsAcrossDesktops(hwnd) continue if (InStr(win_title, "explorer.exe")) { - class_name := WinGetClass("ahk_id " hwnd) + try class_name := WinGetClass("ahk_id " hwnd) + catch + continue if (class_name = "Progman" || class_name = "WorkerW" || class_name = "Shell_TrayWnd") continue } @@ -103,25 +499,48 @@ PickFocusableAppWindow(hwnds, win_title) { if VirtualDesktopEnabled() { desktop_num := GetWindowDesktopNum(hwnd) if (desktop_num <= 0) - allow_invisible := true - else if (desktop_num != VD.getCurrentDesktopNum()) + continue + if (desktop_num > 0 && desktop_num != VD.getCurrentDesktopNum()) allow_invisible := true } if (!allow_invisible && !(style & 0x10000000)) continue - state := WinGetMinMax("ahk_id " hwnd) + try state := WinGetMinMax("ahk_id " hwnd) + catch + continue if (state = -1) continue return hwnd } for hwnd in hwnds { - if !WinExist("ahk_id " hwnd) + if !WindowExistsAcrossDesktops(hwnd) + continue + try state := WinGetMinMax("ahk_id " hwnd) + catch continue - state := WinGetMinMax("ahk_id " hwnd) if (state = -1) return hwnd + if VirtualDesktopEnabled() { + desktop_num := GetWindowDesktopNum(hwnd) + if (desktop_num <= 0) + continue + } + } + return 0 +} + +PickDesktopAssignedWindow(hwnds) { + for hwnd in hwnds { + if !WindowExistsAcrossDesktops(hwnd) + continue + if VirtualDesktopEnabled() { + desktop_num := GetWindowDesktopNum(hwnd) + if (desktop_num <= 0) + continue + } + return hwnd } return 0 } diff --git a/src/lib/virtual_desktop.ahk b/src/lib/virtual_desktop.ahk index e0ba21b..8818a41 100644 --- a/src/lib/virtual_desktop.ahk +++ b/src/lib/virtual_desktop.ahk @@ -26,6 +26,35 @@ VirtualDesktopSwitchOnFocus() { return Config["virtual_desktop"]["switch_on_focus"] } +VirtualDesktopFocusDebugEnabled() { + global Config + if !VirtualDesktopEnabled() + return false + return Config["virtual_desktop"].Has("debug_focus") && Config["virtual_desktop"]["debug_focus"] +} + +EnsureFocusDebugLogInit() { + global focus_debug_log_initialized + if !IsSet(focus_debug_log_initialized) + focus_debug_log_initialized := false + if focus_debug_log_initialized + return + if !VirtualDesktopFocusDebugEnabled() + return + log_dir := GetAppDataDir() + DirCreate(log_dir) + TryResetFocusLogFile(log_dir "\\vd.focus.debug.log") + focus_debug_log_initialized := true +} + +TryResetFocusLogFile(path) { + try { + if FileExist(path) + FileDelete(path) + FileAppend("", path) + } +} + InitVirtualDesktop() { if !VirtualDesktopEnabled() return @@ -34,6 +63,7 @@ InitVirtualDesktop() { VD.createUntil(ensure_count) InitVirtualDesktopTrayIndicator() InitVirtualDesktopAutoAssign() + EnsureFocusDebugLogInit() } InitVirtualDesktopAutoAssign() { @@ -348,9 +378,42 @@ GetCurrentDesktopNumFresh() { GetWindowDesktopNum(hwnd) { if !VirtualDesktopEnabled() return 0 - try return VD.getDesktopNumOfHWND(hwnd) + desktop_num := 0 + try desktop_num := VD.getDesktopNumOfHWND(hwnd) catch as err + desktop_num := 0 + if (desktop_num > 0) { + SetWindowDesktopCache(hwnd, desktop_num) + return desktop_num + } + if (desktop_num < 0) + return desktop_num + cached := GetWindowDesktopCache(hwnd) + if (cached > 0) + return cached + return desktop_num +} + +GetWindowDesktopCache(hwnd) { + global window_desktop_cache + if !IsSet(window_desktop_cache) || !(window_desktop_cache is Map) + window_desktop_cache := Map() + if !window_desktop_cache.Has(hwnd) + return 0 + if !WindowExistsAcrossDesktops(hwnd) { + window_desktop_cache.Delete(hwnd) return 0 + } + return window_desktop_cache[hwnd] +} + +SetWindowDesktopCache(hwnd, desktop_num) { + global window_desktop_cache + if !IsSet(window_desktop_cache) || !(window_desktop_cache is Map) + window_desktop_cache := Map() + if (desktop_num <= 0) + return + window_desktop_cache[hwnd] := desktop_num } IsWindowOnCurrentDesktop(hwnd) { @@ -386,35 +449,56 @@ ActivateWindowAcrossDesktops(hwnd) { if !WindowExistsAcrossDesktops(hwnd) return 0 + if VirtualDesktopFocusDebugEnabled() + LogDesktopFocusDebug("activate_attempt", hwnd, GetWindowDesktopNum(hwnd)) + if VirtualDesktopSwitchOnFocus() { desktop_num := GetWindowDesktopNum(hwnd) - if (desktop_num > 0 && desktop_num != VD.getCurrentDesktopNum()) { + current_desktop := GetCurrentDesktopNumFresh() + if (desktop_num > 0 && desktop_num != current_desktop) { try { VD.goToDesktopOfWindow("ahk_id " hwnd, true) } catch as err { + ; Fallback to direct desktop switch when goToDesktopOfWindow fails. try { - WinActivate "ahk_id " hwnd + if VirtualDesktopFocusDebugEnabled() + LogDesktopFocusDebug("switch_fallback", hwnd, desktop_num) + VD.goToDesktopNum(desktop_num) + VD.WaitDesktopSwitched(desktop_num) } catch + LogDesktopFocusDebug("switch_failed", hwnd, desktop_num) return 0 } - try return WinGetID("A") - catch + try { + if VirtualDesktopFocusDebugEnabled() + LogDesktopFocusDebug("activate_switched", hwnd, desktop_num) + WinActivate "ahk_id " hwnd + return WinGetID("A") + } catch + LogDesktopFocusDebug("activate_failed", hwnd, desktop_num) return 0 } if (desktop_num <= 0) { try { + if VirtualDesktopFocusDebugEnabled() + LogDesktopFocusDebug("switch_unknown", hwnd, desktop_num) VD.goToDesktopOfWindow("ahk_id " hwnd, true) - try return WinGetID("A") - catch + try { + WinActivate "ahk_id " hwnd + return WinGetID("A") + } catch return 0 } catch as err { + LogDesktopFocusDebug("switch_unknown_failed", hwnd, desktop_num) ; fall through to direct activation } } } try { + if VirtualDesktopFocusDebugEnabled() + LogDesktopFocusDebug("activate_direct", hwnd, GetWindowDesktopNum(hwnd)) WinActivate "ahk_id " hwnd } catch return 0 @@ -422,3 +506,20 @@ ActivateWindowAcrossDesktops(hwnd) { catch return 0 } + +LogDesktopFocusDebug(reason, hwnd, desktop_num := 0) { + log_dir := GetAppDataDir() + DirCreate(log_dir) + log_path := log_dir "\\vd.focus.debug.log" + + exe_name := "" + title := "" + class_name := "" + try exe_name := WinGetProcessName("ahk_id " hwnd) + try title := WinGetTitle("ahk_id " hwnd) + try class_name := WinGetClass("ahk_id " hwnd) + + line := "[" A_Now "] " reason " hwnd=" Format("0x{:X}", hwnd) + line .= " desktop=" desktop_num " exe=" exe_name " class=" class_name " title=" title + SafeFileAppend(line "`n", log_path) +} From e3f8d936b9b62c48c7db5d93f221a9b3657aebcd Mon Sep 17 00:00:00 2001 From: Grant Kilber Date: Mon, 2 Mar 2026 21:13:02 -0600 Subject: [PATCH 12/20] fix EnumWindows callback signature error --- src/lib/focus_or_run.ahk | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/lib/focus_or_run.ahk b/src/lib/focus_or_run.ahk index 10b4e8c..dbc5b06 100644 --- a/src/lib/focus_or_run.ahk +++ b/src/lib/focus_or_run.ahk @@ -272,14 +272,18 @@ LogFocusOrRunSnapshotStats(exe_name) { GetAllTopLevelWindows() { hwnds := [] - callback := CallbackCreate(EnumWindowsCallback.Bind(hwnds), "Fast") + global enum_windows_target := hwnds + callback := CallbackCreate(EnumWindowsCallback, "Fast") DllCall("EnumWindows", "Ptr", callback, "Ptr", 0) CallbackFree(callback) + enum_windows_target := "" return hwnds } -EnumWindowsCallback(hwnds, hwnd, lparam) { - hwnds.Push(hwnd) +EnumWindowsCallback(hwnd, lparam) { + global enum_windows_target + if (enum_windows_target is Array) + enum_windows_target.Push(hwnd) return true } From e181a5cf05fffb595614a119630d1c4c49d7e753 Mon Sep 17 00:00:00 2001 From: Grant Kilber Date: Mon, 2 Mar 2026 21:14:51 -0600 Subject: [PATCH 13/20] increase border thickness in example config --- config/config.example.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/config.example.toml b/config/config.example.toml index 808daf6..dba9c04 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -57,7 +57,7 @@ run_paths = [] [apps.vscode-border] match = { exe = "Code.exe", title = " - Visual Studio Code$", title_regex = true } # Optional per-app focus border tweaks. -focus_border = { border_color = "#4D96FF", border_thickness = 3, corner_radius = 12 } +focus_border = { border_color = "#4D96FF", border_thickness = 7, corner_radius = 12 } # Process tree matching (example): split a terminal app and its child tool. # @@ -159,7 +159,7 @@ enabled = true border_color = "#222feb" move_mode_color = "#2ECC71" command_mode_color = "#FFD400" -border_thickness = 4 +border_thickness = 7 corner_radius = 16 update_interval_ms = 20 From e0595895ce0e1834ab3f5e7f5aef68e7a75e2c72 Mon Sep 17 00:00:00 2001 From: Grant Kilber Date: Mon, 2 Mar 2026 21:27:35 -0600 Subject: [PATCH 14/20] add comparison to fancyzones, komorebi, & gazewm --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 1808a62..05a054b 100644 --- a/README.md +++ b/README.md @@ -251,3 +251,19 @@ I really like komorebi--though I didn't use it for long and I have never been ab ### [GlazeWM](https://github.com/glzr-io/glazewm) GlazeWM is another popular tiling window manager for Windows operating systems. + + +### Feature comparison (Harken vs similar Windows tools) + +| Feature | Harken | [FancyZones](https://learn.microsoft.com/en-us/windows/powertoys/fancyzones) | [komorebi](https://github.com/LGUG2Z/komorebi) | [GlazeWM](https://github.com/glzr-io/glazewm) | +| --- | --- | --- | --- | --- | +| Primary interaction model | Keyboard-first hotkeys and commands | Zone-based snapping (mouse + keyboard) | Dynamic tiling WM | Dynamic tiling WM | +| Launch-or-focus app hotkeys | Yes (configurable per app) | No (outside scope) | Possible via external tooling/scripts | Possible via external tooling/scripts | +| Directional focus movement | Yes (`alt + h/j/k/l`) | No | Yes | Yes | +| Stack/window cycling | Yes (`super + [` / `super + ]`, app cycling) | Mostly | Yes (tiling containers/workspaces) | Yes (tiling containers/workspaces) | +| Virtual desktop hotkeys | Yes (native desktop integration) | Indirect (PowerToys + Windows shortcuts) | Yes | Yes | +| Freeform floating adjustments | Yes (move mode + resize controls) | Primarily zone snapping | Primarily tiling; floating is secondary | Primarily tiling; floating is secondary | +| Built-in command/help overlay | Yes (`super + /`, command mode) | No | No built-in overlay | No built-in overlay | +| Config format | TOML | GUI + JSON settings | JSON/YAML-style config | YAML | +| Best fit | Users who want keyboard speed without fully committing to tiling | Users who want quick snap layouts | Users who want full tiling workflows | Users who want full tiling workflows | + From 750f701df2a579c3de74ea930a65cf72f2dd2058 Mon Sep 17 00:00:00 2001 From: Grant Kilber Date: Mon, 2 Mar 2026 21:45:10 -0600 Subject: [PATCH 15/20] feat: scroll to move focus between virtual desktops or move tiles across vritual desktops --- README.md | 2 ++ config/config.example.toml | 1 + harken.ahk | 1 + src/hotkeys/window.ahk | 22 ++++++++++++++++++++++ src/lib/config_loader.ahk | 1 + 5 files changed, 27 insertions(+) diff --git a/README.md b/README.md index 05a054b..2f7e3d0 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,9 @@ Add `debug = true` under `match.process_tree` to log process tree details to | Shortcut | Action | | --- | --- | | `super + alt + h/l` | Previous/next desktop | +| `super + alt + WheelUp/WheelDown` | Previous/next desktop (when `virtual_desktop.scroll_switch = true`) | | `super + alt + shift + h/l` | Move window to previous/next desktop (follow) | +| `super + alt + shift + WheelUp/WheelDown` | Move window to previous/next desktop (when `virtual_desktop.scroll_switch = true`) | | `super + alt + ` | Go to mapped desktop (`[[virtual_desktop.]]`) | | `super + alt + shift + ` | Move window to mapped desktop (follow) | | `virtual_desktop.auto_assign` | Move newly created windows that match `apps[]` with `desktop` set | diff --git a/config/config.example.toml b/config/config.example.toml index dba9c04..36bf16e 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -120,6 +120,7 @@ enabled = true switch_on_focus = true ensure_count = 0 cycle_prefer_current = true +scroll_switch = true # Hotkey fields here use AHK syntax without super (super is implied). # Modifiers are applied by Harken (alt or alt+shift) for desktop actions. prev_hotkey = "h" diff --git a/harken.ahk b/harken.ahk index b361887..05420d5 100644 --- a/harken.ahk +++ b/harken.ahk @@ -131,6 +131,7 @@ DefaultConfig() { "switch_on_focus", true, "ensure_count", 0, "cycle_prefer_current", true, + "scroll_switch", false, "prev_hotkey", "h", "next_hotkey", "l", "move_prev_hotkey", "h", diff --git a/src/hotkeys/window.ahk b/src/hotkeys/window.ahk index 60fa713..d5074bf 100644 --- a/src/hotkeys/window.ahk +++ b/src/hotkeys/window.ahk @@ -10,6 +10,7 @@ center_cycle_hotkey := Config["window"]["center_width_cycle_hotkey"] cycle_app_windows_hotkey := Config["window"]["cycle_app_windows_hotkey"] cycle_app_windows_current_hotkey := Config["window"]["cycle_app_windows_current_hotkey"] vd_config := Config.Has("virtual_desktop") ? Config["virtual_desktop"] : Map() +vd_scroll_switch := vd_config.Has("scroll_switch") ? vd_config["scroll_switch"] : false vd_prev_hotkey := vd_config.Has("prev_hotkey") ? vd_config["prev_hotkey"] : "" vd_next_hotkey := vd_config.Has("next_hotkey") ? vd_config["next_hotkey"] : "" vd_move_prev_hotkey := vd_config.Has("move_prev_hotkey") ? vd_config["move_prev_hotkey"] : "" @@ -883,6 +884,17 @@ if (vd_next_hotkey != "") LogVirtualDesktopAction("goto_relative hotkey=" vd_next_hotkey " delta=1 current=" GetCurrentDesktopNumFresh()), GoToRelativeDesktop(1) )) +if (vd_scroll_switch) { + ; Optional wheel navigation to avoid breaking existing super+wheel resize behavior by default. + Hotkey("*WheelUp", (*) => ( + LogVirtualDesktopAction("goto_relative hotkey=*WheelUp delta=-1 current=" GetCurrentDesktopNumFresh()), + GoToRelativeDesktop(-1) + )) + Hotkey("*WheelDown", (*) => ( + LogVirtualDesktopAction("goto_relative hotkey=*WheelDown delta=1 current=" GetCurrentDesktopNumFresh()), + GoToRelativeDesktop(1) + )) +} LogVirtualDesktopHotkeys("prev_hotkey=" vd_prev_hotkey " next_hotkey=" vd_next_hotkey) for _, entry in vd_goto_hotkeys { if !(entry is Map) @@ -930,6 +942,16 @@ if (vd_move_next_hotkey != "") LogVirtualDesktopAction("move_relative hotkey=" vd_move_next_hotkey " delta=1 current=" GetCurrentDesktopNumFresh()), MoveWindowToRelativeDesktop(1) )) +if (vd_scroll_switch) { + Hotkey("*WheelUp", (*) => ( + LogVirtualDesktopAction("move_relative hotkey=*WheelUp delta=-1 current=" GetCurrentDesktopNumFresh()), + MoveWindowToRelativeDesktop(-1) + )) + Hotkey("*WheelDown", (*) => ( + LogVirtualDesktopAction("move_relative hotkey=*WheelDown delta=1 current=" GetCurrentDesktopNumFresh()), + MoveWindowToRelativeDesktop(1) + )) +} LogVirtualDesktopHotkeys("move_prev_hotkey=" vd_move_prev_hotkey " move_next_hotkey=" vd_move_next_hotkey) for _, entry in vd_move_hotkeys { if !(entry is Map) diff --git a/src/lib/config_loader.ahk b/src/lib/config_loader.ahk index 4f5b848..7df0da8 100644 --- a/src/lib/config_loader.ahk +++ b/src/lib/config_loader.ahk @@ -118,6 +118,7 @@ ConfigSchema() { "switch_on_focus", "bool", "ensure_count", "number", "cycle_prefer_current", "bool", + "scroll_switch", "bool", "prev_hotkey", "string", "next_hotkey", "string", "move_prev_hotkey", "string", From 71ba7b82e13e8c6c5a0aa023091acf7b4ed2c928 Mon Sep 17 00:00:00 2001 From: Grant Kilber Date: Mon, 2 Mar 2026 22:00:33 -0600 Subject: [PATCH 16/20] fix window walker graphics glitch when resolution changes --- src/lib/window_walker.ahk | 69 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 4 deletions(-) diff --git a/src/lib/window_walker.ahk b/src/lib/window_walker.ahk index dfc40bb..f72e15d 100644 --- a/src/lib/window_walker.ahk +++ b/src/lib/window_walker.ahk @@ -14,6 +14,8 @@ class WindowWalker static image_list := "" static icon_cache := Map() static corner_radius := 8 + static last_monitor := "" + static last_monitor_work_area_key := "" static excluded_exes := Map( "applicationframehost", true, "lockapp", true, @@ -32,6 +34,7 @@ class WindowWalker if !Config.Has("window_selector") || !Config["window_selector"]["enabled"] return + WindowWalker.EnsureGuiForMonitorContext() WindowWalker.EnsureGui() WindowWalker.EnsureNavigationHotkeys() WindowWalker.RefreshWindows() @@ -89,6 +92,39 @@ class WindowWalker WindowWalker.gui.OnEvent("Close", (*) => WindowWalker.Hide()) } + static EnsureGuiForMonitorContext() + { + ; Recreate the GUI when the active monitor work area changes so stale DPI/layout state + ; does not keep clipping content after monitor detach/attach or scale transitions. + monitor := WindowWalker.GetActiveMonitorNumber() + work_area_key := WindowWalker.GetMonitorWorkAreaKey(monitor) + if !WindowWalker.gui { + WindowWalker.last_monitor := monitor + WindowWalker.last_monitor_work_area_key := work_area_key + return + } + + if (WindowWalker.last_monitor = monitor && WindowWalker.last_monitor_work_area_key = work_area_key) + return + + WindowWalker.DestroyGui() + WindowWalker.last_monitor := monitor + WindowWalker.last_monitor_work_area_key := work_area_key + } + + static DestroyGui() + { + if WindowWalker.gui { + try WindowWalker.gui.Destroy() + } + + WindowWalker.gui := "" + WindowWalker.search_edit := "" + WindowWalker.list_view := "" + WindowWalker.image_list := "" + WindowWalker.icon_cache := Map() + } + static ShowCentered() { WindowWalker.gui.Show("Hide AutoSize") @@ -98,6 +134,9 @@ class WindowWalker pos_x := left + (right - left - w) / 2 pos_y := top + (bottom - top - h) / 2 WindowWalker.gui.Show("x" pos_x " y" pos_y) + ; Query final dimensions after positioning so rounded-corner clipping uses current + ; monitor scaling instead of stale dimensions captured from another monitor context. + WindowWalker.gui.GetPos(,, &w, &h) WindowWalker.ApplyRoundedCorners(w, h) } @@ -105,11 +144,16 @@ class WindowWalker { if !WindowWalker.gui return - if (!width || !height) { - WindowWalker.gui.GetPos(,, &width, &height) - } - if (width <= 0 || height <= 0) + ; Always refresh dimensions from the live window so the region is never based on stale + ; size values from a previous monitor/DPI context. + WindowWalker.gui.GetPos(,, &width, &height) + + if (width <= 0 || height <= 0) { + ; Clear any old clip region instead of leaving the GUI content partially cropped. + try WinSetRegion(, WindowWalker.gui) return + } + radius := WindowWalker.corner_radius try WinSetRegion("0-0 w" width " h" height " r" radius "-" radius, WindowWalker.gui) } @@ -460,17 +504,34 @@ class WindowWalker } static GetActiveWorkArea(&left, &top, &right, &bottom) + { + mon := WindowWalker.GetActiveMonitorNumber() + if !mon + mon := MonitorGetPrimary() + MonitorGetWorkArea(mon, &left, &top, &right, &bottom) + } + + static GetActiveMonitorNumber() { mon := "" + hwnd := "" try hwnd := WinGetID("A") if hwnd { try mon_handle := DllCall("MonitorFromWindow", "Ptr", hwnd, "UInt", 2, "Ptr") if mon_handle mon := WindowWalker.ConvertMonitorHandleToNumber(mon_handle) } + if !mon + mon := MonitorGetPrimary() + return mon + } + + static GetMonitorWorkAreaKey(mon) + { if !mon mon := MonitorGetPrimary() MonitorGetWorkArea(mon, &left, &top, &right, &bottom) + return mon "|" left "|" top "|" right "|" bottom } static ConvertMonitorHandleToNumber(handle) From 275008b4ccb5624ce1a712a2e03fa16a37dc5e70 Mon Sep 17 00:00:00 2001 From: Grant Kilber Date: Tue, 3 Mar 2026 00:13:18 -0600 Subject: [PATCH 17/20] feat: show clickable hints with {super + .} --- LICENSES/UIA.ahk-LICENSE.md | 21 + README.md | 18 + config/config.example.toml | 112 +- harken.ahk | 16 + src/hotkeys/screen_search.ahk | 8 + src/hotkeys/unbound.ahk | 3 + src/lib/UIA.ahk | 7946 +++++++++++++++++++++++++++++++++ src/lib/command_toast.ahk | 3 + src/lib/config_loader.ahk | 9 + src/lib/screen_search.ahk | 632 +++ 10 files changed, 8758 insertions(+), 10 deletions(-) create mode 100644 LICENSES/UIA.ahk-LICENSE.md create mode 100644 src/hotkeys/screen_search.ahk create mode 100644 src/lib/UIA.ahk create mode 100644 src/lib/screen_search.ahk diff --git a/LICENSES/UIA.ahk-LICENSE.md b/LICENSES/UIA.ahk-LICENSE.md new file mode 100644 index 0000000..eabd922 --- /dev/null +++ b/LICENSES/UIA.ahk-LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Descolada + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 2f7e3d0..b5f968e 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Harken allows a keyboard-centered workflow on Windows: a single super modifier, - Cycle stacked tiles with `super + [` and `super + ]`. - Cycle focus between tiles of the same program with `super + c`. - Show a hotkey menu with `super + /`. +- Screen search hints with `super + .`. - Virtual desktops (native) - Navigate between desktops with better hotkeys - Assign custom hotkeys for the index-based virtual desktops. @@ -67,6 +68,7 @@ Other - `super + alt` sends `ctrl + tab` (configurable via `global_hotkeys`) - `super + c` cycle through windows of the same app - `super + shift + c` cycle through windows of the same app on the current desktop +- `super + .` screen search (click hints) - `super + alt + h/l` switch to previous/next virtual desktop - `super + alt + shift + h/l` move the active window to previous/next desktop (follow) - `super + w` open Window Selector (fuzzy find open windows) @@ -143,6 +145,7 @@ Add `debug = true` under `match.process_tree` to log process tree details to | --- | --- | | `super + /` | Show command overlay (temporary) | | `super + w` | Window selector (window walker) | +| `super + .` | Screen search (click hints) | | `super + c` | Cycle app windows across desktops | | `super + shift + c` | Cycle app windows on current desktop | | `super + space` | Center width cycle | @@ -221,6 +224,19 @@ enabled = false interval_ms = 2500 ``` +### Screen search + +```toml +[screen_search] +enabled = true +hotkey = "." +hint_chars = "asdfghjklqwertyuiopzxcvbnm" +max_results = 200 +min_size_px = 12 +min_distance_px = 40 +hint_opacity = 235 +``` + ## Limitations - This has not been tested with multi-monitor setups or much outside of ultra-wide monitors. - Virtual desktop integration requires AutoHotkey v2.1-alpha-18 or later. @@ -233,6 +249,8 @@ interval_ms = 2500 - License: `LICENSES/JXON_ahk2-LICENSE.md` - VD.ahk from https://github.com/FuPeiJiang/VD.ahk - License: `LICENSES/VD.ahk-LICENSE.md` +- UIA.ahk v1.1.2 from https://github.com/Descolada/UIA-v2 + - License: `LICENSES/UIA.ahk-LICENSE.md` ## Similar tools and inspirations diff --git a/config/config.example.toml b/config/config.example.toml index 36bf16e..2746552 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -1,17 +1,44 @@ config_version = 1 + +# ============================================================= +# Harken configuration (example + reference) +# +# This file is intended to be the authoritative guide for +# available settings and their usage. Copy it to your config +# location and edit as needed. +# ============================================================= + # Use one or more keys as super (CapsLock is the default). +# In this example either key would work. super_key = ["F24", "CapsLock"] +# ------------------------------------------------------------- +# Apps: launch-or-focus bindings + optional matching rules +# +# You can define apps as named tables under [apps] or as array +# entries with [[apps]]. Named tables use the table key as id and +# should NOT set `id` inside. +# +# Match sources (choose one): +# - win_title: AHK window match string (e.g., "ahk_exe Code.exe") +# - match: Map with exe/class/title and optional regex flags +# +# Matching notes: +# - match.*_regex = true treats the corresponding field as regex +# - exclude_titles is a list of regex patterns (case-insensitive) +# - process_tree lets you match by ancestor/descendant executables +# ------------------------------------------------------------- + [apps] -# Named tables: key is the app id; do not set `id` inside. [apps.browser] hotkey = "f" win_title = "ahk_exe floorp.exe" run = "floorp" +run_paths = [] # desktop = 2 # follow_on_spawn = true -run_paths = [] +# exclude_titles = [".*SilverBullet.*", ".*ChatGPT.*"] [apps.files] hotkey = "e" @@ -23,8 +50,9 @@ run_paths = [] hotkey = "v" win_title = "ahk_exe Code.exe" run = "code" -# run_start_in = "C:\\projects" run_paths = [] +# run_start_in = "C:\\projects" +# desktop = 2 [apps.terminal] hotkey = "s" @@ -52,11 +80,10 @@ win_title = "ahk_exe discord.exe" run = "discord" run_paths = [] # exclude_titles = ["^Discord Updater$", "^Discord Canary$"] -# desktop = 2 +# Match-only override (no hotkey): tweak focus border by title. [apps.vscode-border] match = { exe = "Code.exe", title = " - Visual Studio Code$", title_regex = true } -# Optional per-app focus border tweaks. focus_border = { border_color = "#4D96FF", border_thickness = 7, corner_radius = 12 } # Process tree matching (example): split a terminal app and its child tool. @@ -71,6 +98,18 @@ focus_border = { border_color = "#4D96FF", border_thickness = 7, corner_radius = # hotkey = "y" # match = { exe = "alacritty.exe", process_tree = { mode = "descendant", exe = ["yazi.exe"], debug = true } } +# process_tree options: +# - mode: "ancestor" | "descendant" | "either" (default: descendant) +# - exe: list of executables to search for (required) +# - exe_regex: treat exe entries as regex (default: false) +# - max_depth: limit traversal depth (0 = unlimited) +# - negate: invert result (useful for “all except …”) +# - debug: log details to %APPDATA%\harken\process_tree.debug.log + +# ------------------------------------------------------------- +# Global hotkeys (optional) +# ------------------------------------------------------------- + [[global_hotkeys]] enabled = true hotkey = "Alt" @@ -79,31 +118,57 @@ target_exes = [] # Will require virtual desktop hotkeys to be used without pressing super first in the combo. send_keys = "^{Tab}" +# ------------------------------------------------------------- +# Window management +# ------------------------------------------------------------- + [window] resize_step = 20 move_step = 20 super_double_tap_ms = 300 + # Cycle app windows across all desktops. -cycle_app_windows_hotkey = "c" +cycle_app_windows_hotkey = "+c" # Cycle app windows on the current desktop only. -cycle_app_windows_current_hotkey = "+c" - # Swap the two values if you prefer current-desktop cycling on `super+c`. +cycle_app_windows_current_hotkey = "c" center_width_cycle_hotkey = "Space" +# minimize_others_hotkey = "-" [window.move_mode] enable = true cancel_key = "Esc" +# ------------------------------------------------------------- +# Window selector (window walker) +# ------------------------------------------------------------- + [window_selector] enabled = true hotkey = "w" -max_results = 12 +max_results = 30 title_preview_len = 60 match_title = true match_exe = true include_minimized = true close_on_focus_loss = true +# ------------------------------------------------------------- +# Screen search (click hints) +# ------------------------------------------------------------- + +[screen_search] +enabled = true +hotkey = "." +hint_chars = "asdfghjklqwertyuiopzxcvbnm" +max_results = 200 +min_size_px = 12 +min_distance_px = 40 +hint_opacity = 235 + +# ------------------------------------------------------------- +# Window manager (grid + gap) +# ------------------------------------------------------------- + [window_manager] grid_size = 3 gap_px = 0 @@ -115,27 +180,38 @@ left = 4 right = 4 bottom = 5 +# ------------------------------------------------------------- +# Virtual desktops +# ------------------------------------------------------------- + [virtual_desktop] enabled = true switch_on_focus = true ensure_count = 0 cycle_prefer_current = true + +# Optional mouse wheel switching (super+alt+wheel). +# Keep disabled if you use super+wheel for resize elsewhere. scroll_switch = true + # Hotkey fields here use AHK syntax without super (super is implied). # Modifiers are applied by Harken (alt or alt+shift) for desktop actions. prev_hotkey = "h" next_hotkey = "l" move_prev_hotkey = "h" move_next_hotkey = "l" + # Debug logs write to %APPDATA%\harken when enabled. debug_cycle = false debug_hotkeys = false # debug_focus = false + tray_indicator = true tray_format = "{current}/{total}" + +# auto_assign applies only to apps with `desktop` set. # auto_assign = false # auto_assign_interval_ms = 500 -# auto_assign applies only to apps with `desktop` set. [[virtual_desktop.1]] hotkey = "1" @@ -143,6 +219,10 @@ hotkey = "1" [[virtual_desktop.2]] hotkey = "2" +# ------------------------------------------------------------- +# Directional focus +# ------------------------------------------------------------- + [directional_focus] # Directional focus was tricky to make good enough. enabled = true @@ -155,6 +235,10 @@ perpendicular_overlap_min = 0.2 cross_monitor = false debug_enabled = false +# ------------------------------------------------------------- +# Focus border +# ------------------------------------------------------------- + [focus_border] enabled = true border_color = "#222feb" @@ -164,10 +248,18 @@ border_thickness = 7 corner_radius = 16 update_interval_ms = 20 +# ------------------------------------------------------------- +# Helper overlay +# ------------------------------------------------------------- + [helper] enabled = true overlay_opacity = 200 +# ------------------------------------------------------------- +# Config watcher +# ------------------------------------------------------------- + [config_watch] enabled = false # Only used if WinAPI method has to fall back to polling. diff --git a/harken.ahk b/harken.ahk index 05420d5..6a8bf3c 100644 --- a/harken.ahk +++ b/harken.ahk @@ -9,6 +9,8 @@ #Include src/lib/virtual_desktop.ahk #Include src/lib/focus_or_run.ahk #Include src/lib/command_toast.ahk +#Include src/lib/UIA.ahk +#Include src/lib/screen_search.ahk #Include src/lib/window_inspector.ahk config_dir := GetConfigDir() @@ -68,6 +70,7 @@ SetWinDelay(-1) #Include src/hotkeys/window.ahk #Include src/hotkeys/directional_focus.ahk #Include src/hotkeys/window_walker.ahk +#Include src/hotkeys/screen_search.ahk #Include src/hotkeys/unbound.ahk DefaultConfig() { @@ -111,6 +114,15 @@ DefaultConfig() { "include_minimized", true, "close_on_focus_loss", true ), + "screen_search", Map( + "enabled", true, + "hotkey", ".", + "hint_chars", "asdfghjklqwertyuiopzxcvbnm", + "max_results", 200, + "min_size_px", 12, + "min_distance_px", 40, + "hint_opacity", 235 + ), "config_watch", Map( "enabled", false, "interval_ms", 2500 @@ -412,6 +424,10 @@ ExecuteCommand(callback) { } OnSuperKeyDown() { + if ScreenSearch.IsActive() { + ScreenSearch.Hide() + return + } if WindowWalker.IsActive() { WindowWalker.Hide() return diff --git a/src/hotkeys/screen_search.ahk b/src/hotkeys/screen_search.ahk new file mode 100644 index 0000000..b857983 --- /dev/null +++ b/src/hotkeys/screen_search.ahk @@ -0,0 +1,8 @@ +; Screen search hotkey binding. +global Config + +if Config.Has("screen_search") && Config["screen_search"]["enabled"] { + hotkey_name := Config["screen_search"]["hotkey"] + if (hotkey_name != "") + RegisterSuperComboHotkey(hotkey_name, (*) => ScreenSearch.Toggle()) +} diff --git a/src/hotkeys/unbound.ahk b/src/hotkeys/unbound.ahk index 7a4d2cf..8d945cd 100644 --- a/src/hotkeys/unbound.ahk +++ b/src/hotkeys/unbound.ahk @@ -17,6 +17,9 @@ RegisterUnboundHotkeys() { AddUsedKey(used_keys, "o") AddUsedKey(used_keys, "n") + if Config.Has("screen_search") && Config["screen_search"]["enabled"] + AddUsedKey(used_keys, Config["screen_search"]["hotkey"]) + for _, app in Config["apps"] { if app.Has("hotkey") && app["hotkey"] != "" AddUsedKey(used_keys, app["hotkey"]) diff --git a/src/lib/UIA.ahk b/src/lib/UIA.ahk new file mode 100644 index 0000000..e78c0e5 --- /dev/null +++ b/src/lib/UIA.ahk @@ -0,0 +1,7946 @@ +/* + Introduction & credits + This library implements Microsoft's UI Automation framework. + Microsoft's official documentation:: https://docs.microsoft.com/en-us/windows/win32/winauto/entry-uiauto-win32 + Author: Descolada + Credits: thqby, neptercn (v1), jethrow (AHK v1 UIA library) + + A lot of modifications have been added to the original UIA framework, such as custom methods for elements (eg element.Click()) + + Usage: + All main UIA methods and properties can be accessed through the UIA variable. + Eg UIA.GetRootElement() + Additionally some extra variables are initialized: + UIA.IUIAutomationVersion contains the version number of IUIAutomation interface + UIA.TrueCondition contains a TrueCondition + UIA.TreeWalkerTrue contains a TreeWalker with the TrueCondition + UIA is initialized only once, on the first use of UIA. + The initalized IUIAutomation version depends on the system, but the most recent version is automatically used. + If the version needs to be limited, then set the IUIAutomationMaxVersion global variable before the first use of UIA. + On initialization, SPI_SETSCREENREADER is also called to notify the system of UIA use. This is needed for some programs (such as VSCode). + + UIAutomation constants and enumerations are also available in the UIA variable. Eg. UIA.Property.Name contains the UIA_NamePropertyId value. + Microsoft documentation for constants and enumerations: + UI Automation Constants: https://docs.microsoft.com/en-us/windows/win32/winauto/uiauto-entry-constants + UI Automation Enumerations: https://docs.microsoft.com/en-us/windows/win32/winauto/uiauto-entry-enumerations +*/ +/* + Questions: + - if method returns a SafeArray, should we return a Wrapped SafeArray, Raw SafeArray, or AHK Array. Currently we return wrapped AHK arrays for SafeArrays. Although SafeArrays are more convenient to loop over, this causes more confusion in users who are not familiar with SafeArrays (questions such as why are they 0-indexed not 1-indexed, why doesnt for k, v in SafeArray work properly etc). + - on UIA Interface conversion methods, how should the data be returned? wrapped/extracted or raw? should raw data be a ByRef param? + - Cached Members? + - UIA Element existance - dependent on window being visible (non minimized), and also sometimes Elements are lazily generated (eg Microsoft Teams, when a meeting is started then the toolbar buttons (eg Mute, react) aren't visible to UIA, but hovering over them with the cursor or calling ElementFromPoint causes Teams to generate and make them visible to UIA. + - better way of supporting differing versions of IUIAutomation (version 2, 3, 4) + - Get methods vs property getter: currently we use properties when the item stores data, fetching the data is "cheap" and when it doesn't have side-effects, and in computationally expensive cases use Get...(). + - should ElementFromHandle etc methods have activateChromiumAccessibility set to True or False? Currently is True, because Chromium apps are very common, and checking whether its on should be relatively fast. + + Design choices: + - ElementArrays are converted to AHK arrays automatically. This is because speed-testing has shown + that they are either equal, or AHK array comes on top by far. The situation where it loses to the + native one is when we need to get exactly one element from the array and won't use it more. This + is optimized by using the native ElementArray in cases of Element[integer]. + + To-do: + - Better error handling +*/ + + +if !A_IsCompiled && A_LineFile = A_ScriptFullPath + UIA.Viewer() + +class UIA { +; Semantic version of the UIA library +static Version => "1.1.2" +/** + * First use of UIA variable initiates UIA, UIA.IUIAutomationVersion, UIA.TrueCondition and + * UIA.TreeWalkerTrue. Also enables screen reader with SPI_SETSCREENREADER. + * Initialized IUIAutomation version can be altered with changing the global IUIAutomationMaxVersion + * variable, which by default is set to the latest available UIA version. + */ +static __New() { + global IUIAutomationMaxVersion := IUIAutomationMaxVersion ?? 7 + , IUIAutomationActivateScreenReader := IUIAutomationActivateScreenReader ?? 1 + , IUIAutomationDllPath := IUIAutomationDllPath ?? "" + this.IUIAutomationVersion := IUIAutomationMaxVersion+1, this.ptr := 0 + ; The following is a partial head-way into making the UIA class name-agnostic (that is, making it possible to rename and reference UIA more easily, eg when wrapped inside inside another class). + ; UIA static methods have been converted this way, but IUIAutomationElement runs into a problem: namely the syntax highlighter is unable to detect that IUIAutomationElement has been made to + ; extend IUIAutomationBase with this.IUIAutomationElement.base := this.IUIAutomationBase and thus shows "problems" for each UIA.IUIAutomationElement() call + this.prototype.__UIA := this ; Allows referencing UIA static without using the class name + , this.Cleanup.prototype.__UIA := this ; inject reference to UIA in Cleanup. This must be before the next line, since Cleanup gets initiated there + static __IID := { + IUIAutomation:"{30cbe57d-d9d0-452a-ab13-7ac5ac4825ee}", + IUIAutomation2:"{34723aff-0c9d-49d0-9896-7ab52df8cd8a}", + IUIAutomation3:"{73d768da-9b51-4b89-936e-c209290973e7}", + IUIAutomation4:"{1189c02a-05f8-4319-8e21-e817e3db2860}", + IUIAutomation5:"{25f700c8-d816-4057-a9dc-3cbdee77e256}", + IUIAutomation6:"{aae072da-29e3-413d-87a7-192dbf81ed10}", + IUIAutomation7:"{29de312e-83c6-4309-8808-e8dfcb46c3c2}", + IClassFactory:"{00000001-0000-0000-c000-000000000046}" + }, + __CLSID := { + CUIAutomation:"{ff48dba4-60ef-4201-aa87-54103eef594e}", + CUIAutomation8:"{e22ad333-b25f-460c-83d0-0581107395c9}" + }, __Cleanup := this.Cleanup() + + if (IUIAutomationDllPath != "") { + hModule := DllCall("LoadLibrary", "Str", IUIAutomationDllPath, "Ptr") + if !hModule + throw Error("Failed to load custom UIA DLL",, IUIAutomationDllPath) + pDllGetClassObject := DllCall("GetProcAddress", "Ptr", hModule, "AStr", "DllGetClassObject", "Ptr") + if !pDllGetClassObject + throw Error("Failed to get custom UIA DllGetClassObject") + DllCall("ole32\CLSIDFromString", "Str", __CLSID.CUIAutomation8, "Ptr", CLSID_CUIAutomation8 := Buffer(16)) + DllCall("ole32\IIDFromString", "Str", __IID.IClassFactory, "Ptr", IID_IClassFactory := Buffer(16)) + + if DllCall(pDllGetClassObject, "Ptr", CLSID_CUIAutomation8, "Ptr", IID_IClassFactory, "Ptr*", ppFactory := ComValue(13, 0)) + goto RegularCUI + + while (--this.IUIAutomationVersion > 1) { + if !__IID.HasOwnProp("IUIAutomation" this.IUIAutomationVersion) + continue + try { + DllCall("ole32\IIDFromString", "Str", __IID.IUIAutomation%(this.IUIAutomationVersion)%, "Ptr", IID := Buffer(16)) + ComCall(3, ppFactory, "Ptr", 0, "Ptr", IID, "Ptr*", &ppAutomation:=0) + if !ppAutomation + continue + this.ptr := ppAutomation + break + } + } + + RegularCUI: + if !this.HasOwnProp("ptr") || (this.HasOwnProp("ptr") && !this.ptr) { + DllCall("ole32\CLSIDFromString", "Str", __CLSID.CUIAutomation, "Ptr", CLSID_CUIAutomation := Buffer(16)) + if DllCall(pDllGetClassObject, "Ptr", CLSID_CUIAutomation, "Ptr", IID_IClassFactory, "Ptr*", ppFactory := ComValue(13, 0)) + throw Error("Failed to get CUIAutomation class factory") + DllCall("ole32\IIDFromString", "Str", __IID.IUIAutomation, "Ptr", IID := Buffer(16)) + ComCall(3, ppFactory, "Ptr", 0, "Ptr", IID, "Ptr*", &ppAutomation:=0) + if !ppAutomation + throw Error("Failed to initialize UIA from custom DLL") + this.ptr := ppAutomation + } + } else { + while (--this.IUIAutomationVersion > 1) { + if !__IID.HasOwnProp("IUIAutomation" this.IUIAutomationVersion) + continue + try { + this.ptr := ComObjValue(this.__ := ComObject(__CLSID.CUIAutomation8, __IID.IUIAutomation%(this.IUIAutomationVersion)%)) + break + } + } + ; If all else fails, try the first IUIAutomation version + if !this.HasOwnProp("ptr") || (this.HasOwnProp("ptr") && !this.ptr) + this.ptr := ComObjValue(this.__ := ComObject(__CLSID.CUIAutomation, __IID.IUIAutomation)) + } + this.TrueCondition := this.CreateTrueCondition(), this.TreeWalkerTrue := this.CreateTreeWalker(this.TrueCondition) + ; Define some properties that shouldn't be included in the value->name Map + this.Property[30000], this.Property.T := 30003, this.Property.ControlType := 30003, this.Property.N := 30005, this.Property.CN := 30012, this.Property.A := 30011 +} +class Cleanup { + __New() { + global IUIAutomationActivateScreenReader + this.ScreenReaderStartingState := this.__UIA.GetScreenReader() + if IUIAutomationActivateScreenReader && !this.ScreenReaderStartingState + this.__UIA.SetScreenReader(1) + } + __Delete() { + global IUIAutomationActivateScreenReader + try this.__UIA.RemoveAllEventHandlers() + if IUIAutomationActivateScreenReader && !this.ScreenReaderStartingState + DllCall("user32.dll\SystemParametersInfo", "uint", 0x0047, "uint", this.ScreenReaderStartingState, "int", 0, "uint", 2) ; SPI_SETSCREENREADER + } +} + +; ---------- IUIAutomation constants and enumerations. ---------- + +class Enumeration { + ; This is enables getting property names from values using the array style + __Item[param] { + get { + local k, v + if !this.HasOwnProp("__CachedValues") { + this.__CachedValues := Map() + for k, v in this.OwnProps() + this.__CachedValues[v] := k + } + if this.__CachedValues.Has(param) + return this.__CachedValues[param] + throw UnsetItemError("Property item `"" param "`" not found!", -2) + } + } + HasValue(val) { + if !this.HasOwnProp("__CachedValues") + try this[0] + if this.__CachedValues.Has(val) + return this.__CachedValues[val] + return 0 + } +} + +; Access properties with UIA.property.subproperty (UIA.Type.Button) +; To get the property name from value, use the array style: UIA.property[value] (UIA.Type[50000] == "Button") + +; enum MatchMode Contains string comparison options used in various methods involving element comparison with condition. +static MatchMode := {StartsWith:1, Substring:2, Exact:3, RegEx:"RegEx", base:this.Enumeration.Prototype} +; enum CaseSense Contains CaseSense options used in various methods involving element comparison with condition. +static CaseSense := {Off:0, On:1, Default:1, base:this.Enumeration.Prototype} +; enum Type (alias ControlType) Contains UI element types, which are used to identify the capabilities of a control and to determine how to interact with it. +static Type := {Button:50000,Calendar:50001,CheckBox:50002,ComboBox:50003,Edit:50004,Link:50005, Hyperlink:50005,Image:50006,ListItem:50007,List:50008,Menu:50009,MenuBar:50010,MenuItem:50011,ProgressBar:50012,RadioButton:50013,ScrollBar:50014,Slider:50015,Spinner:50016,StatusBar:50017,Tab:50018,TabItem:50019,Text:50020,ToolBar:50021,ToolTip:50022,Tree:50023,TreeItem:50024,Custom:50025,Group:50026,Thumb:50027,DataGrid:50028,DataItem:50029,Document:50030,SplitButton:50031,Window:50032,Pane:50033,Header:50034,HeaderItem:50035,Table:50036,TitleBar:50037,Separator:50038,SemanticZoom:50039,AppBar:50040, base:this.Enumeration.Prototype} +static ControlType := this.Type ; Alias definition +; enum Pattern Contains interface names used to control a particular aspect of a control's functionality. These can be used to retrieve information about a particular capability of the control, or to manipulate a particular aspect of the control's behavior. +static Pattern := {Invoke: 10000, Selection: 10001, Value: 10002, RangeValue: 10003, Scroll: 10004, ExpandCollapse: 10005, Grid: 10006, GridItem: 10007, MultipleView: 10008, Window: 10009, SelectionItem: 10010, Dock: 10011, Table: 10012, TableItem: 10013, Text: 10014, Toggle: 10015, Transform: 10016, ScrollItem: 10017, LegacyIAccessible: 10018, ItemContainer: 10019, VirtualizedItem: 10020, SynchronizedInput: 10021, ObjectModel: 10022, Annotation: 10023, Text2:10024, Styles: 10025, Spreadsheet: 10026, SpreadsheetItem: 10027, Transform2: 10028, TextChild: 10029, Drag: 10030, DropTarget: 10031, TextEdit: 10032, CustomNavigation: 10033, Selection2: 10034, base:this.Enumeration.Prototype} +; enum Event Contains event types evoked by changes in the user interface. Listeners to these can be registered with CreateEventHandler. +static Event := {ToolTipOpened:20000,ToolTipClosed:20001,StructureChanged:20002,MenuOpened:20003,AutomationPropertyChanged:20004,AutomationFocusChanged:20005,AsyncContentLoaded:20006,MenuClosed:20007,LayoutInvalidated:20008,Invoke_Invoked:20009,SelectionItem_ElementAddedToSelection:20010,SelectionItem_ElementRemovedFromSelection:20011,SelectionItem_ElementSelected:20012,Selection_Invalidated:20013,Text_TextSelectionChanged:20014,Text_TextChanged:20015,Window_WindowOpened:20016,Window_WindowClosed:20017,MenuModeStart:20018,MenuModeEnd:20019,InputReachedTarget:20020,InputReachedOtherElement:20021,InputDiscarded:20022,SystemAlert:20023,LiveRegionChanged:20024,HostedFragmentRootsInvalidated:20025,Drag_DragStart:20026,Drag_DragCancel:20027,Drag_DragComplete:20028,DropTarget_DragEnter:20029,DropTarget_DragLeave:20030,DropTarget_Dropped:20031,TextEdit_TextChanged:20032,TextEdit_ConversionTargetChanged:20033,Changes:20034,Notification:20035,ActiveTextPositionChanged:20036, base:this.Enumeration.Prototype} +; enum Property Contains property names and ids that expose properties on UI Automation elements +static Property := {RuntimeId:30000,Id:30000,BoundingRectangle:30001,Location:30001,ProcessId:30002,ControlType:30003,Type:30003,LocalizedControlType:30004, LocalizedType:30004,Name:30005,AcceleratorKey:30006,AccessKey:30007,HasKeyboardFocus:30008,IsKeyboardFocusable:30009,IsEnabled:30010,AutomationId:30011,ClassName:30012,HelpText:30013,ClickablePoint:30014,Culture:30015,IsControlElement:30016,IsContentElement:30017,LabeledBy:30018,IsPassword:30019,NativeWindowHandle:30020,ItemType:30021,IsOffscreen:30022,Orientation:30023,FrameworkId:30024,IsRequiredForForm:30025,ItemStatus:30026,IsDockPatternAvailable:30027,IsExpandCollapsePatternAvailable:30028,IsGridItemPatternAvailable:30029,IsGridPatternAvailable:30030,IsInvokePatternAvailable:30031,IsMultipleViewPatternAvailable:30032,IsRangeValuePatternAvailable:30033,IsScrollPatternAvailable:30034,IsScrollItemPatternAvailable:30035,IsSelectionItemPatternAvailable:30036,IsSelectionPatternAvailable:30037,IsTablePatternAvailable:30038,IsTableItemPatternAvailable:30039,IsTextPatternAvailable:30040,IsTogglePatternAvailable:30041,IsTransformPatternAvailable:30042,IsValuePatternAvailable:30043,IsWindowPatternAvailable:30044,ValueValue:30045,Value:30045,ValueIsReadOnly:30046,RangeValueValue:30047,RangeValueIsReadOnly:30048,RangeValueMinimum:30049,RangeValueMaximum:30050,RangeValueLargeChange:30051,RangeValueSmallChange:30052,ScrollHorizontalScrollPercent:30053,ScrollHorizontalViewSize:30054,ScrollVerticalScrollPercent:30055,ScrollVerticalViewSize:30056,ScrollHorizontallyScrollable:30057,ScrollVerticallyScrollable:30058,SelectionSelection:30059,SelectionCanSelectMultiple:30060,SelectionIsSelectionRequired:30061,GridRowCount:30062,GridColumnCount:30063,GridItemRow:30064,GridItemColumn:30065,GridItemRowSpan:30066,GridItemColumnSpan:30067,GridItemContainingGrid:30068,DockDockPosition:30069,ExpandCollapseExpandCollapseState:30070,MultipleViewCurrentView:30071,MultipleViewSupportedViews:30072,WindowCanMaximize:30073,WindowCanMinimize:30074,WindowWindowVisualState:30075,WindowWindowInteractionState:30076,WindowIsModal:30077,WindowIsTopmost:30078,SelectionItemIsSelected:30079,SelectionItemSelectionContainer:30080,TableRowHeaders:30081,TableColumnHeaders:30082,TableRowOrColumnMajor:30083,TableItemRowHeaderItems:30084,TableItemColumnHeaderItems:30085,ToggleToggleState:30086,TransformCanMove:30087,TransformCanResize:30088,TransformCanRotate:30089,IsLegacyIAccessiblePatternAvailable:30090,LegacyIAccessibleChildId:30091,LegacyIAccessibleName:30092,LegacyIAccessibleValue:30093,LegacyIAccessibleDescription:30094,LegacyIAccessibleRole:30095,LegacyIAccessibleState:30096,LegacyIAccessibleHelp:30097,LegacyIAccessibleKeyboardShortcut:30098,LegacyIAccessibleSelection:30099,LegacyIAccessibleDefaultAction:30100,AriaRole:30101,AriaProperties:30102,IsDataValidForForm:30103,ControllerFor:30104,DescribedBy:30105,FlowsTo:30106,ProviderDescription:30107,IsItemContainerPatternAvailable:30108,IsVirtualizedItemPatternAvailable:30109,IsSynchronizedInputPatternAvailable:30110,OptimizeForVisualContent:30111,IsObjectModelPatternAvailable:30112,AnnotationAnnotationTypeId:30113,AnnotationAnnotationTypeName:30114,AnnotationAuthor:30115,AnnotationDateTime:30116,AnnotationTarget:30117,IsAnnotationPatternAvailable:30118,IsTextPattern2Available:30119,StylesStyleId:30120,StylesStyleName:30121,StylesFillColor:30122,StylesFillPatternStyle:30123,StylesShape:30124,StylesFillPatternColor:30125,StylesExtendedProperties:30126,IsStylesPatternAvailable:30127,IsSpreadsheetPatternAvailable:30128,SpreadsheetItemFormula:30129,SpreadsheetItemAnnotationObjects:30130,SpreadsheetItemAnnotationTypes:30131,IsSpreadsheetItemPatternAvailable:30132,Transform2CanZoom:30133,IsTransformPattern2Available:30134,LiveSetting:30135,IsTextChildPatternAvailable:30136,IsDragPatternAvailable:30137,DragIsGrabbed:30138,DragDropEffect:30139,DragDropEffects:30140,IsDropTargetPatternAvailable:30141,DropTargetDropTargetEffect:30142,DropTargetDropTargetEffects:30143,DragGrabbedItems:30144,Transform2ZoomLevel:30145,Transform2ZoomMinimum:30146,Transform2ZoomMaximum:30147,FlowsFrom:30148,IsTextEditPatternAvailable:30149,IsPeripheral:30150,IsCustomNavigationPatternAvailable:30151,PositionInSet:30152,SizeOfSet:30153,Level:30154,AnnotationTypes:30155,AnnotationObjects:30156,LandmarkType:30157,LocalizedLandmarkType:30158,FullDescription:30159,FillColor:30160,OutlineColor:30161,FillType:30162,VisualEffects:30163,OutlineThickness:30164,CenterPoint:30165,Rotation:30166,Size:30167,IsSelectionPattern2Available:30168,Selection2FirstSelectedItem:30169,Selection2LastSelectedItem:30170,Selection2CurrentSelectedItem:30171,Selection2ItemCount:30173,IsDialog:30174, base:this.Enumeration.Prototype} +; enum PropertyVariantType Internally used enum, mapping property ids to variant types +static PropertyVariantType := Map(30000,0x2003,30001,0x2005,30002,3,30003,3,30004,8,30005,8,30006,8,30007,8,30008,0xB,30009,0xB,30010,0xB,30011,8,30012,8,30013,8,30014,0x2005,30015,3,30016,0xB,30017,0xB,30018,0xD,30019,0xB,30020,3,30021,8,30022,0xB,30023,3,30024,8,30025,0xB,30026,8,30027,0xB,30028,0xB,30029,0xB,30030,0xB,30031,0xB,30032,0xB,30033,0xB,30034,0xB,30035,0xB,30036,0xB,30037,0xB,30038,0xB,30039,0xB,30040,0xB,30041,0xB,30042,0xB,30043,0xB,30044,0xB,30045,8,30046,0xB,30047,5,30048,0xB,30049,5,30050,5,30051,5,30052,5,30053,5,30054,5,30055,5,30056,5,30057,0xB,30058,0xB,30059,0x200D,30060,0xB,30061,0xB,30062,3,30063,3,30064,3,30065,3,30066,3,30067,3,30068,0xD,30069,3,30070,3,30071,3,30072,0x2003,30073,0xB,30074,0xB,30075,3,30076,3,30077,0xB,30078,0xB,30079,0xB,30080,0xD,30081,0x200D,30082,0x200D,30083,0x2003,30084,0x200D,30085,0x200D,30086,3,30087,0xB,30088,0xB,30089,0xB,30090,0xB,30091,3,30092,8,30093,8,30094,8,30095,3,30096,3,30097,8,30098,8,30099,0x200D,30100,8,30101,8,30102,8,30103,0xB,30104,0xD,30105,0xD,30106,0xD,30107,8,30108,0xB,30109,0xB,30110,0xB,30111,0xB,30112,0xB,30113,3,30114,8,30115,8,30116,8,30117,0xD,30118,0xB,30119,0xB,30120,3,30121,8,30122,3,30123,8,30124,8,30125,3,30126,8,30127,0xB,30128,0xB,30129,8,30130,0x200D,30131,0x2003,30132,0xB,30133,0xB,30134,0xB,30135,3,30136,0xB,30137,0xB,30138,0xB,30139,8,30140,0x2008,30141,0xB,30142,8,30143,0x2008,30144,0x200D,30145,5,30146,5,30147,5,30148,0x200D,30149,0xB,30150,0xB,30151,0xB,30152,3,30153,3,30154,3,30155,0x2003,30156,0x2003,30157,3,30158,8,30159,8,30160,3,30161,0x2003,30162,3,30163,3,30164,0x2005,30165,0x2005,30166,5,30167,0x2005,30168,0xB) +static PropertyVariantTypeBSTR := Map(30004,8,30005,8,30006,8,30007,8,30011,8,30012,8,30013,8,30021,8,30024,8,30026,8,30045,8,30092,8,30093,8,30094,8,30097,8,30098,8,30100,8,30101,8,30102,8,30107,8,30114,8,30115,8,30116,8,30121,8,30123,8,30124,8,30126,8,30129,8,30139,8,30142,8,30158,8,30159,8) +; enum TextAttribute Contains format and style properties (text attributes) of textual content +static TextAttribute := {AnimationStyle:40000,BackgroundColor:40001,BulletStyle:40002,CapStyle:40003,Culture:40004,FontName:40005,FontSize:40006,FontWeight:40007,ForegroundColor:40008,HorizontalTextAlignment:40009,IndentationFirstLine:40010,IndentationLeading:40011,IndentationTrailing:40012,IsHidden:40013,IsItalic:40014,IsReadOnly:40015,IsSubscript:40016,IsSuperscript:40017,MarginBottom:40018,MarginLeading:40019,MarginTop:40020,MarginTrailing:40021,OutlineStyles:40022,OverlineColor:40023,OverlineStyle:40024,StrikethroughColor:40025,StrikethroughStyle:40026,Tabs:40027,TextFlowDirections:40028,UnderlineColor:40029,UnderlineStyle:40030,AnnotationTypes:40031,AnnotationObjects:40032,StyleName:40033,StyleId:40034,Link:40035,IsActive:40036,SelectionActiveEnd:40037,CaretPosition:40038,CaretBidiMode:40039,LineSpacing:40040,BeforeParagraphSpacing:40041,AfterParagraphSpacing:40042,SayAsInterpretAs:40043, base:this.Enumeration.Prototype} +; enum AttributeVariantType Internally used enum, mapping TextAttribute ids to variant types +static AttributeVariantType := {40000:3,40001:3,40002:3,40003:3,40004:3,40005:8,40006:5,40007:3,40008:3,40009:3,40010:5,40011:5,40012:5,40013:0xB,40014:0xB,40015:0xB,40016:0xB,40017:0xB,40018:5,40019:5,40020:5,40021:5,40022:3,40023:3,40024:3,40025:3,40026:3,40027:0x2005,40028:3,40029:3,40030:3,40031:0x2003,40032:0x200D,40033:8,40034:3,40035:0xD,40036:0xB,40037:3,40038:3,40039:3,40040:8,40041:5,40042:5,40043:8} +; enum AnnotationType Contains values that are used to identify types of annotations in a document. +static AnnotationType := {Unknown:60000,SpellingError:60001,GrammarError:60002,Comment:60003,FormulaError:60004,TrackChanges:60005,Header:60006,Footer:60007,Highlighted:60008,Endnote:60009,Footnote:60010,InsertionChange:60011,DeletionChange:60012,MoveChange:60013,FormatChange:60014,UnsyncedChange:60015,EditingLockedChange:60016,ExternalChange:60017,ConflictingChange:60018,Author:60019,AdvancedProofingIssue:60020,DataValidationError:60021,CircularReferenceError:60022,Mathematics:60023, base:this.Enumeration.Prototype} +; enum Style Contains values used to identify the visual style of text in a document. +static Style := {Custom:70000,Heading1:70001,Heading2:70002,Heading3:70003,Heading4:70004,Heading5:70005,Heading6:70006,Heading7:70007,Heading8:70008,Heading9:70009,Title:70010,Subtitle:70011,Normal:70012,Emphasis:70013,Quote:70014,BulletedList:70015,NumberedList:70016, base:this.Enumeration.Prototype} +; enum LandmarkType Contains values used to identify landmark types. +static LandmarkType := {Custom:80000,Form:80001,Main:80002,Navigation:80003,Search:80004, base:this.Enumeration.Prototype} +; enum HeadingLevel Contains values to indicate the heading level of a UIA element +static HeadingLevel := {None:80050, 1:80051, 2:80052, 3:80053, 4:80054, 5:80055, 6:80056, 7:80057, 8:80058, 9:80059, base:this.Enumeration.Prototype} +; enum Change Contains values used to identify UIA change information. +static Change := {Summary:90000, base:this.Enumeration.Prototype} +; enum Metadata Contains values used to identify metadata types in a document. +static Metadata := {SayAsInterpretAs:100000, base:this.Enumeration.Prototype} +; enum AsyncContentLoadedState Contains values that describe the progress of asynchronous loading of content. +static AsyncContentLoadedState := {Beginning:0, Progress:1, Completed:2, base:this.Enumeration.Prototype} +; enum AutomationIdentifierType Deprecated: Contains values used in the UiaLookupId function. +static AutomationIdentifierType := {Property:0,Pattern:1,Event:2,Type:3,ControlType:3,TextAttribute:4,LandmarkType:5,Annotation:6,Changes:7,Style:7, base:this.Enumeration.Prototype} +; enum ConditionType Contains values that specify a type of UIA condition. +static ConditionType := {True:0,False:1,Property:2,And:3,Or:4,Not:5, base:this.Enumeration.Prototype} +; enum EventArgsType Contains values that specify the event type described by a UiaEventArgs structure. +static EventArgsType := {Simple:0,PropertyChanged:1,StructureChanged:2,AsyncContentLoaded:3,WindowClosed:4,TextEditTextChanged:5,Changes:6,Notification:7,ActiveTextPositionChanged:8,StructuredMarkup:9, base:this.Enumeration.Prototype} +; enum AutomationElementMode Contains values that specify the type of reference to use when returning UIA elements. This is used in CreateCacheRequest. +static AutomationElementMode := {None:0, Full:1, base:this.Enumeration.Prototype} +; enum CoalesceEventsOptions Contains possible values for the CoalesceEvents property, which indicates whether to receive all events, or a subset where duplicate events are detected and filtered. +static CoalesceEventsOptions := {Disabled:0, Enabled:1, base:this.Enumeration.Prototype} +; enum ConnectionRecoveryBehaviorOptions Contains possible values for the ConnectionRecoveryBehavior property, which indicates whether an accessible technology client adjusts provider request timeouts when the provider is non-responsive. +static ConnectionRecoveryBehaviorOptions := {Disabled:0, Enabled:1, base:this.Enumeration.Prototype} +; enum PropertyConditionFlags Contains values used in creating property conditions with CreatePropertyConditionEx. In condition objects, these can be used with the "flags" key. +static PropertyConditionFlags := {None:0, IgnoreCase:1, MatchSubstring:2, IgnoreCaseMatchSubstring:3, base:this.Enumeration.Prototype} +; enum TreeScope Contains values that specify the scope of various operations in the UIA tree. +static TreeScope := {None: 0, Element: 1, Children: 2, Family:3, Descendants: 4, ElementDescendants: 5, Subtree: 7, Parent: 8, Ancestors: 16, base:this.Enumeration.Prototype} +; enum TreeTraversalOptions Contains values that can be used to customize tree navigation order. +static TreeTraversalOptions := {Default:0, PostOrder:1, LastToFirstOrder:2, LastToFirstPostOrder:3, base:this.Enumeration.Prototype} +; enum TreeTraversalDirection Not currently used. +static TreeTraversalDirection := {Up:0, Down:1, Left:2, Right:3, Next:3, Previous:2, FirstChild:1, LastChild:4, Parent:0, Normalize:5} + + +; enum ActiveEnd Contains possible values for the SelectionActiveEnd text attribute, which indicates the location of the caret relative to a text range that represents the currently selected text. +static ActiveEnd := {None:0,Start:1,End:2, base:this.Enumeration.Prototype} +; enum AnimationStyle Contains values for the AnimationStyle text attribute. +static AnimationStyle := {None:0,LasVegasLights:1,BlinkingBackground:2,SparkleText:3,MarchingBlackAnts:4,MarchingRedAnts:5,Shimmer:6,Other:-1, base:this.Enumeration.Prototype} +; enum BulletStyle Contains values for the BulletStyle text attribute. +static BulletStyle := {None:0,HollowRoundBullet:1,FilledRoundBullet:2,HollowSquareBullet:3,FilledSquareBullet:4,DashBullet:5,Other:-1, base:this.Enumeration.Prototype} +; enum CapStyle Contains values that specify the value of the CapStyle text attribute. +static CapStyle := {None:0,SmallCap:1,AllCap:2,AllPetiteCaps:3,PetiteCaps:4,Unicase:5,Titling:6,Other:-1, base:this.Enumeration.Prototype} +; enum CaretBidiMode Contains possible values for the CaretBidiMode text attribute, which indicates whether the caret is in text that flows from left to right, or from right to left. +static CaretBidiMode := {LTR:0,RTL:1, base:this.Enumeration.Prototype} +; enum CaretPosition Contains possible values for the CaretPosition text attribute, which indicates the location of the caret relative to a line of text in a text range. +static CaretPosition := {Unknown:0,EndOfLine:1,BeginningOfLine:2, base:this.Enumeration.Prototype} +; enum DockPosition Contains values that specify the location of a docking window represented by the Dock control pattern. +static DockPosition := {Top:0,Left:1,Bottom:2,Right:3,Fill:4,None:5, base:this.Enumeration.Prototype} +; enum ExpandCollapseState Contains values that specify the state of a UI element that can be expanded and collapsed. +static ExpandCollapseState := {Collapsed:0,Expanded:1,PartiallyExpanded:2,LeafNode:3, base:this.Enumeration.Prototype} +; enum FillType Contains values for the FillType attribute. +static FillType := {None:0,Color:1,Gradient:2,Picture:3,Pattern:4, base:this.Enumeration.Prototype} +; enum FlowDirection Contains values for the TextFlowDirections text attribute. +static FlowDirection := {Default:0,RightToLeft:1,BottomToTop:2,Vertical:4, base:this.Enumeration.Prototype} +; enum LiveSetting Contains possible values for the LiveSetting property. This property is implemented by provider elements that are part of a live region. +static LiveSetting := {Off:0,Polite:1,Assertive:2, base:this.Enumeration.Prototype} +; enum NavigateDirection Contains values used to specify the direction of navigation within the Microsoft UI Automation tree. +static NavigateDirection := {Parent:0,NextSibling:1,PreviousSibling:2,FirstChild:3,LastChild:4, base:this.Enumeration.Prototype} +; enum NotificationKind Defines values that indicate the type of a notification event, and a hint to the listener about the processing of the event. +static NotificationKind := {ItemAdded:0,ItemRemoved:1,ActionCompleted:2,ActionAborted:3,Other:4, base:this.Enumeration.Prototype} +; enum NotificationProcessing Defines values that indicate how a notification should be processed. +static NotificationProcessing := {ImportantAll:0,ImportantMostRecent:1,All:2,MostRecent:3,CurrentThenMostRecent:4, base:this.Enumeration.Prototype} +; enum OrientationType Contains values that specify the orientation of a control. +static OrientationType := {None:0,Horizontal:1,Vertical:2, base:this.Enumeration.Prototype} +; enum OutlineStyles Contains values for the OutlineStyle text attribute. +static OutlineStyles := {None:0,Outline:1,Shadow:2,Engraved:4,Embossed:8, base:this.Enumeration.Prototype} +; enum ProviderOptions +static ProviderOptions := {ClientSideProvider:1,ServerSideProvider:2,NonClientAreaProvider:4,OverrideProvider:8,ProviderOwnsSetFocus:10,UseComThreading:20,RefuseNonClientSupport:40,HasNativeIAccessible:80,UseClientCoordinates:100, base:this.Enumeration.Prototype} +; enum RowOrColumnMajor Contains values that specify whether data in a table should be read primarily by row or by column. +static RowOrColumnMajor := {RowMajor:0,ColumnMajor:1,Indeterminate:2, base:this.Enumeration.Prototype} +; enum SayAsInterpretAs Defines the values that indicate how a text-to-speech engine should interpret specific data. +static SayAsInterpretAs := {None:0,Spell:1,Cardinal:2,Ordinal:3,Number:4,Date:5,Time:6,Telephone:7,Currency:8,Net:9,Url:10,Address:11,Name:13,Media:14,Date_MonthDayYear:15,Date_DayMonthYear:16,Date_YearMonthDay:17,Date_YearMonth:18,Date_MonthYear:19,Date_DayMonth:20,Date_MonthDay:21,Date_Year:22,Time_HoursMinutesSeconds12:23,Time_HoursMinutes12:24,Time_HoursMinutesSeconds24:25,Time_HoursMinutes24:26, base:this.Enumeration.Prototype} +; enum ScrollAmount Contains values that specify the direction and distance to scroll. +static ScrollAmount := {NoScroll:-1,LargeDecrement:0,SmallDecrement:1,NoAmount:2,LargeIncrement:3,SmallIncrement:4, base:this.Enumeration.Prototype} +; enum StructureChangeType Contains values that specify the type of change in the Microsoft UI Automation tree structure. +static StructureChangeType := {ChildAdded:0,ChildRemoved:1,ChildrenInvalidated:2,ChildrenBulkAdded:3,ChildrenBulkRemoved:4,ChildrenReordered:5, base:this.Enumeration.Prototype} +; enum SupportedTextSelection Contains values that specify the supported text selection attribute. +static SupportedTextSelection := {None:0,Single:1,Multiple:2, base:this.Enumeration.Prototype} +; enum SynchronizedInputType Contains values that specify the type of synchronized input. +static SynchronizedInputType := {KeyUp:1,KeyDown:2,LeftMouseUp:4,LeftMouseDown:8,RightMouseUp:10,RightMouseDown:20, base:this.Enumeration.Prototype} +; enum TextDecorationLineStyle Contains values that specify the OverlineStyle, StrikethroughStyle, and UnderlineStyle text attributes. +static TextDecorationLineStyle := {None:0, Single:1, WordsOnly:2, Double:3, Dot:4, Dash:5, DashDot:6, DashDotDot:7, Wavy:8, ThickSingle:9, DoubleWavy:11, ThickWavy:12, LongDash:13, ThickDash:14, ThickDashDot:15, ThickDashDotDot:16, ThickDot:17, ThickLongDash:18, Other:-1, base:this.Enumeration.Prototype} +; enum TextEditChangeType Describes the text editing change being performed by controls when text-edit events are raised or handled. +static TextEditChangeType := {None:0,AutoCorrect:1,Composition:2,CompositionFinalized:3,AutoComplete:4, base:this.Enumeration.Prototype} +; enum TextPatternRangeEndpoint Contains values that specify the endpoints of a text range. +static TextPatternRangeEndpoint := {Start:0,End:1, base:this.Enumeration.Prototype} +; enum TextUnit Contains values that specify units of text for the purposes of navigation. +static TextUnit := {Character:0,Format:1,Word:2,Line:3,Paragraph:4,Page:5,Document:6, base:this.Enumeration.Prototype} +; enum ToggleState Contains values that specify the toggle state of a Microsoft UI Automation element that implements the Toggle control pattern. +static ToggleState := {Off:0,On:1,Indeterminate:2, base:this.Enumeration.Prototype} +; enum ZoomUnit Contains possible values for the IUIAutomationTransformPattern2::ZoomByUnit method, which zooms the viewport of a control by the specified unit. +static ZoomUnit := {NoAmount:0,LargeDecrement:1,SmallDecrement:2,LargeIncrement:3,SmallIncrement:4, base:this.Enumeration.Prototype} +; enum WindowVisualState Contains values that specify the visual state of a window. +static WindowVisualState := {Normal:0,Maximized:1,Minimized:2, base:this.Enumeration.Prototype} +; enum WindowInteractionState Contains values that specify the current state of the window for purposes of user interaction. +static WindowInteractionState := {Running:0,Closing:1,ReadyForUserInteraction:2,BlockedByModalWindow:3,NotResponding:4, base:this.Enumeration.Prototype} + +; MSAA/Acc enums +class LegacyIAccessible extends UIA.Enumeration { + static SelectionFlag := {None:0x0, TakeFocus:0x1, TakeSelection:0x2, ExtendSelection:0x4, AddSelection:0x8, RemoveSelection:0x10, Valid:0x1f} + static Role := {TitleBar:0x1,MenuBar:0x2, ScrollBar:0x3, Grip:0x4, Sound:0x5, Cursor:0x6, Caret:0x7, Alert:0x8, Window:0x9, Client:0xa, MenuPopup:0xb, MenuItem:0xc, ToolTip:0xd, Application:0xe, Document:0xf, Pane:0x10, Chart:0x11, Dialog:0x12, Border:0x13, Grouping:0x14, Separator:0x15, Toolbar:0x16, StatusBar:0x17, Table:0x18, ColumnHeader:0x19, RowHeader:0x1a, Column:0x1b, Row:0x1c, Cell:0x1d, Link:0x1e, HelpBalloon:0x1f, Character:0x20, List:0x21, ListItem:0x22, Outline:0x23, OutlineItem:0x24, PageTab:0x25, PropertyPage:0x26, Indicator:0x27, Graphic:0x28, StaticText:0x29, Text:0x2a, PushButton:0x2b, CheckButton:0x2c, RadioButton:0x2d, ComboBox:0x2e, Droplist:0x2f, Progressbar:0x30, Dial:0x31, HotkeyField:0x32, Slider:0x33, SpinButton:0x34, Diagram:0x35, Animation:0x36, Equation:0x37, ButtonDropdown:0x38, ButtonMenu:0x39, ButtonDropdownGrid:0x3a, Whitespace:0x3b, PageTabList:0x3c, Clock:0x3d, SplitButton:0x3e, IPAddress:0x3f, OutlineButton:0x40} + static State := {Normal:0, Unavailable:0x1, Selected:0x2, Focused:0x4, Pressed:0x8, Checked:0x10, Mixed:0x20, Indeterminate:0x20, ReadOnly:0x40, HotTracked:0x80, Default:0x100, Expanded:0x200, Collapsed:0x400, Busy:0x800, Floating:0x1000, Marqueed:0x2000, Animated:0x4000, Invisible:0x8000, Offscreen:0x10000, Sizeable:0x20000, Moveable:0x40000, SelfVoicing:0x80000, Focusable:0x100000, Selectable:0x200000, Linked:0x400000, Traversed:0x800000, MultiSelectable:0x1000000, ExtSelectable:0x2000000, Alert_Low:0x4000000, Alert_Medium:0x8000000, Alert_High:0x10000000, Protected:0x20000000, Valid:0x7fffffff} +} + +; ---------- UIA Properties ---------- + +; Check whether a certain version of UIAutomation is available +static IsIUIAutomation2Available := VerCompare(A_OSVersion, ">=6.2.9200") +static IsIUIAutomation3Available := VerCompare(A_OSVersion, ">=6.3.9600") +static IsIUIAutomation4Available := VerCompare(A_OSVersion, ">=10.0.14393") +static IsIUIAutomation5Available := VerCompare(A_OSVersion, ">=10.0.14393") +static IsIUIAutomation6Available := VerCompare(A_OSVersion, ">=10.0.17763") + +; Check whether a certain version of UIAutomation Element is available +static IsIUIAutomationElement2Available := VerCompare(A_OSVersion, ">=6.2.9200") +static IsIUIAutomationElement3Available := VerCompare(A_OSVersion, ">=6.3.9600") +static IsIUIAutomationElement4Available := VerCompare(A_OSVersion, ">=10") +static IsIUIAutomationElement5Available := VerCompare(A_OSVersion, ">=10.0.15063") +static IsIUIAutomationElement6Available := VerCompare(A_OSVersion, ">=10.0.15063") +static IsIUIAutomationElement7Available := VerCompare(A_OSVersion, ">=10.0.15063") +static IsIUIAutomationElement8Available := VerCompare(A_OSVersion, ">=10.0.17134") +static IsIUIAutomationElement9Available := VerCompare(A_OSVersion, ">=10.0.17763") + +; Retrieves a predefined TreeWalker made with the TrueCondition +static TreeWalkerTrue := "" + +; Retrieves a predefined condition that selects all elements. +static TrueCondition := "" + +; Retrieves a static token object representing a property or text attribute that is not supported. This property is read-only. +; This object can be used for comparison with the results from UIA.IUIAutomationElement,,GetPropertyValue or IUIAutomationTextRange,,GetAttributeValue. +static ReservedNotSupportedValue { + get { + local notSupportedValue + return (ComCall(54, this, "ptr*", ¬SupportedValue := 0), ComValue(0xd, notSupportedValue)) + } +} + +; Retrieves a static token object representing a text attribute that is a mixed attribute. This property is read-only. +; The object retrieved by IUIAutomation,,ReservedMixedAttributeValue can be used for comparison with the results from IUIAutomationTextRange,,GetAttributeValue to determine if a text range contains more than one value for a particular text attribute. +static ReservedMixedAttributeValue { + get { + local mixedAttributeValue + return (ComCall(55, this, "ptr*", &mixedAttributeValue := 0), ComValue(0xd, mixedAttributeValue)) + } +} +; Retrieves an IUIAutomationTreeWalker interface used to discover control elements. +static ControlViewWalker { + get { + static walker := (ComCall(14, this, "ptr*", &walker := 0), walker?this.IUIAutomationTreeWalker(walker):"") + return walker + } +} + +; Retrieves an IUIAutomationTreeWalker interface used to discover content elements. +static ContentViewWalker { + get { + static walker := (ComCall(15, this, "ptr*", &walker := 0), walker?this.IUIAutomationTreeWalker(walker):"") + return walker + } +} + +; Retrieves a tree walker object used to traverse an unfiltered view of the UI Automation tree. +static RawViewWalker { + get { + static walker := (ComCall(16, this, "ptr*", &walker := 0), walker?this.IUIAutomationTreeWalker(walker):"") + return walker + } +} + +; Retrieves a predefined IUIAutomationCondition interface that selects all UI elements in an unfiltered view. +static RawViewCondition { + get { + static condition := (ComCall(17, this, "ptr*", &condition := 0), condition?this.IUIAutomationCondition(condition):"") + return condition + } +} + +; Retrieves a predefined IUIAutomationCondition interface that selects control elements. +static ControlViewCondition { + get { + static condition := (ComCall(18, this, "ptr*", &condition := 0), condition?this.IUIAutomationCondition(condition):"") + return condition + } +} + +; Retrieves a predefined IUIAutomationCondition interface that selects content elements. +static ContentViewCondition { + get { + static condition := (ComCall(19, this, "ptr*", &condition := 0), condition?this.IUIAutomationCondition(condition):"") + return condition + } +} + +; ---------- IUIAutomation2 properties ---------- + +; Specifies whether calls to UI Automation control pattern methods automatically set focus to the target element. Default is True. +static AutoSetFocus { + get { + local out + return (ComCall(58, this, "int*", &out:=0), out) + } + set => ComCall(59, this, "int", Value) +} +; Specifies the length of time that UI Automation will wait for a provider to respond to a client request for an automation element. Default is 20000ms (20 seconds), minimum seems to be 50ms. +static ConnectionTimeout { + get { + local out + return (ComCall(60, this, "int*", &out:=0), out) + } + set => ComCall(61, this, "int", Value) ; Minimum seems to be 50 (ms?) +} +; Specifies the length of time that UI Automation will wait for a provider to respond to a client request for information about an automation element. Default is 2000ms (2 seconds), minimum seems to be 50ms. +static TransactionTimeout { + get { + local out + return (ComCall(62, this, "int*", &out:=0), out) + } + set => ComCall(63, this, "int", Value) +} + +; ---------- IUIAutomation6 properties ---------- + +; Indicates whether an accessible technology client adjusts provider request timeouts when the provider is non-responsive. +ConnectionRecoveryBehavior { + get { + local out + return (ComCall(73, this, "int*", &out), out) + } + set => ComCall(74, this, "int", value) +} +; Gets or sets whether an accessible technology client receives all events, or a subset where duplicate events are detected and filtered. +CoalesceEvents { + get { + local out + return (ComCall(75, this, "int*", &out:=0), out) + } + set => ComCall(76, this, "int", value) +} + +; ---------- IUIAutomation methods ---------- + +/** + * Encodes an array of conditions consisting of only Type and index properties to an UIA path, + * which is a string of characters/numbers/symbols in base64 encoding. + * @param path The array of conditions + * @returns {String} + */ +static EncodePath(path) { + /* + FYI, this is probably the most disgusting algorithm I have ever created. + + It goes like this: + The input (path variable) is an array of conditions, which can only have the Type property and an index. + This gets encoded into a string of characters. This is a base64 character set of A-Z, a-z, + 0-9, and /?<>= with the exception of letters "p" and "P". This is to maintain backwards + compatability with the WalkTree paths, where "p" signifies "parent". + Now, since there are 45 different Types, I've allocated the first 50 encoding characters + (hopefully no more that 5 will be added in the future to UIA) to the Types. + If the condition contains an index as well, then the start of an index is signaled by an + encoding character 50 to 59. 50 means index 2, because index 1 doesn't have to be encoded (it's the default value). + Then the next symbols will be added in base15 to calculate the final number. + Negative indices are 60 to 64, and also the next symbols will be subtracted in base15 + to calculate the final index. + Why only 10 symbols for indices 2-11, yet 5 for negative ones? No particular reason other + than it seems pragmatic to include more positive integers than negative to be encoded + with only 1 character. Currently UIAViewer doesn't even use negative indices, users + may create them themselves. + Note that this encoding doesn't allow any other properties (eg AutomationId, Name etc), + although this support might be added in later versions. + + Example: "VE0q" => [{Type:"Document"}, {Type:"ScrollBar"}, {Type:"Button", index:2}] + "V" corresponds in Base64IntToChar to 31 which is Type Document, "E" is ScrollBar, + "0" is Button, "q" is 50 which is the first index number, and positive indices start with 2. + */ + local out, i, module + static Base64IntToChar := Map(0,'0',1,'1',2,'2',3,'3',4,'4',5,'5',6,'6',7,'7',8,'8',9,'9',10,'A',11,'B',12,'C',13,'D',14,'E',15,'F',16,'G',17,'H',18,'I',19,'J',20,'K',21,'L',22,'M',23,'N',24,'O',25,'Q',26,'R',27,'S',28,'T',29,'U',30,'V',31,'W',32,'X',33,'Y',34,'Z',35,'a',36,'b',37,'c',38,'d',39,'e',40,'f',41,'g',42,'h',43,'i',44,'j',45,'k',46,'l',47,'m',48,'n',49,'o',50,'q',51,'r',52,'s',53,'t',54,'u',55,'v',56,'w',57,'x',58,'y',59,'z',60,'/',61,'?',62,'<',63,'>',64,'=') + if !(path is Array) + path := [path] + out := "" + for cond in path { + t := cond.HasOwnProp("T") ? cond.t : cond.HasOwnProp("Type") ? cond.Type : cond.HasOwnProp("ControlType") ? cond.ControlType : -1 + if t = -1 + throw ValueError("Encodable path requires a Type", -1) + if !(IsInteger(t) && t < 50) + t := this.TypeValidation.Type(t) - 50000 + out .= Base64IntToChar[t] + i := cond.HasOwnProp("i") ? cond.i : cond.HasOwnProp("index") ? cond.index : 1 + if i != 0 && i != 1 { + ; 50 = index 2 + ; 60 = index -1 + ; 61 = index -2 + ; 62 = index -3 + ; 63 = index -4 + ; 64 = index -5 + if i > 1 { + i -= 2, factor := 10 + Loop { + if A_index = 1 { + out .= Base64IntToChar[50 + (module := Mod(i, factor))] + } else { + out .= Base64IntToChar[50 + (module := Mod(i, factor*15))//factor] + factor := factor * 15 + } + i -= module + } Until i = 0 + } else { + i := -i - 1 + out .= Base64IntToChar[60 + (module := Mod(i, 5))] + i -= module, factor := 5 + Loop { + if A_index > 1 { + out .= Base64IntToChar[50 + (module := Mod(i, factor*15))//factor] + i -= module, factor := factor*15 + } + } Until i = 0 + } + } + } + return out +} + +/** + * Decodes an UIA path to an array of conditions (consisting of only Type and index properties). + * @param path A string UIA path + * @returns {Array} + */ +static DecodePath(path) { + static Base64CharToInt := Map('0',0,'1',1,'2',2,'3',3,'4',4,'5',5,'6',6,'7',7,'8',8,'9',9,'A',10,'B',11,'C',12,'D',13,'E',14,'F',15,'G',16,'H',17,'I',18,'J',19,'K',20,'L',21,'M',22,'N',23,'O',24,'Q',25,'R',26,'S',27,'T',28,'U',29,'V',30,'W',31,'X',32,'Y',33,'Z',34,'a',35,'b',36,'c',37,'d',38,'e',39,'f',40,'g',41,'h',42,'i',43,'j',44,'k',45,'l',46,'m',47,'n',48,'o',49,'q',50,'r',51,'s',52,'t',53,'u',54,'v',55,'w',56,'x',57,'y',58,'z',59,'/',60,'?',61,'<',62,'>',63,'=',64) + local out := [], calculatingIndex := 0, cond := "" + loop parse path { + num := Base64CharToInt[A_LoopField] + if calculatingIndex != 0 && num < 50 + calculatingIndex := 0 + if num > 49 && calculatingIndex = 0 { + cond.DefineProp("i", {value:num < 60 ? num-48 : 59-num}) ; 50 => index 2 (index 1 would not exist) ; 60 => index -1 + calculatingIndex := num < 60 ? 1 : -1 + continue + } + if calculatingIndex { + if calculatingIndex > 0 { + cond.i += (num-50)*10*15**(calculatingIndex-1) + calculatingIndex++ + } else { + cond.i -= (num-50)*5*15**(-calculatingIndex-1) + calculatingIndex-- + } + } else { + if cond + out.Push(cond) + cond := {Type:50000+num} + } + } + if cond + out.Push(cond) + return out +} + +static RuntimeIdToString(runtimeId) { + local str := "", v + for v in runtimeId + str .= "." Format("{:X}", v) + return LTrim(str, ".") +} + +static RuntimeIdFromString(str) { + local t := StrSplit(str, "."), v, arr := ComObjArray(3, t.Length) + for v in t + arr[A_Index - 1] := Integer("0x" v) + return arr +} + +/** + * Filters elements from an element array if "function" evaluates to True. + * The resulting elements will have an Index property added which is the index in the original unfiltered array. + * @param elementArray Array of elements + * @param function A function that accepts one parameter (an element) and validates it against some condition. + * @returns {Array} + */ +static Filter(elementArray, function) { + local i, el, ret := [] + if !InStr(Type(elementArray), "Array") + throw TypeError("elementArray must be an Array or IUIAutomationElementArray", -1) + for i, el in elementArray + if function(el) + el.DefineProp("Index", {value: i}), ret.Push(el) + return ret +} + +/** + * Can be used to set or get the current scripts DPI awareness to fix problems with invalid coordinates in + * multi-monitor setups. + * Possible values (read more: https://learn.microsoft.com/en-us/windows/win32/hidpi/dpi-awareness-context): + * DPI_AWARENESS_CONTEXT_UNAWARE = -1 + * DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = -2 (default) + * DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = -3 + * DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4 + * DPI_AWARENESS_CONTEXT_UNAWARE_GDISCALED = -5 + * + */ +static DPIAwareness { + set => DllCall("SetThreadDpiAwarenessContext", "ptr", value, "ptr") + get { + local context := DllCall("GetThreadDpiAwarenessContext") + Loop 5 { + if DllCall("AreDpiAwarenessContextsEqual", "ptr", context, "ptr", -A_Index) + return -A_index + } + return context + } +} + +; Sets the maximum possible DPI awareness level depending on Windows version +static SetMaximumDPIAwareness() => this.DPIAwareness := this.IsIUIAutomationElement6Available ? -4 : -3 + +static ProcessIsElevated(vPID) { + local hProc, hToken, vIsElevated, vRet + ;PROCESS_QUERY_LIMITED_INFORMATION := 0x1000 + if !(hProc := DllCall("OpenProcess", "UInt", 0x1000, "Int",0, "UInt",vPID, "Ptr")) + return -1 + ;TOKEN_QUERY := 0x8 + if !(DllCall("advapi32\OpenProcessToken", "Ptr", hProc, "UInt",0x8, "Ptr*", &hToken:=0)) { + DllCall("CloseHandle", "Ptr", hProc) + return -1 + } + ;TokenElevation := 20 + vRet := (DllCall("advapi32\GetTokenInformation", "Ptr", hToken, "Int", 20, "UInt*", &vIsElevated:=0, "UInt", 4, "UInt*", &vSize:=0)) + DllCall("CloseHandle", "Ptr",hToken) + DllCall("CloseHandle", "Ptr",hProc) + return vRet ? vIsElevated : -1 +} + +/** + * Finds the first element matching a condition from an AHK array + * @param elementArray The array to search + * @param condition The condition the element must match, or a callback function (takes 1 argument, the evaluated element, and returns true/false) + * @param index Looks for the n-th element matching the condition. Negative index starts the search from the end. + * @param startingElement Optional: The element to start the search with + * @param cacheRequest Optional: A cache request to cache the found element with + * @param cached Whether the condition match uses cached properties or not + * @returns {UIA.IUIAutomationElement} + */ +static FindElementFromArray(elementArray, condition, index:=1, startingElement:=0, cacheRequest:=0, cached:=False) { + local scope := 0, order := 0, callback := 0 + condition := this.IUIAutomationElement.__ExtractConditionNamedParameters(condition, &scope, &order, &startingElement, , &index, &callback) + index := this.TypeValidation.Integer(index, "Index"), startingElement := this.TypeValidation.Element(startingElement) + callback := callback || UIA.IUIAutomationElement.Prototype.ValidateCondition.Bind(, condition, cached) + if index = 0 + throw ValueError("Condition index cannot be 0", -1) + if startingElement + startingElement := startingElement.RuntimeId + if index > 0 { + for element in elementArray { + if (startingElement ? (startingElement = element.RuntimeId ? !(startingElement := "") && (scope & 1) : 0) : 1) && callback(element) && --index = 0 + return cacheRequest ? element.BuildUpdatedCache(cacheRequest) : element + } + } else { + index := -index, len := elementArray.Length + 1 + Loop len-1 { + element := elementArray[len-A_index] + if (startingElement ? (startingElement = element.RuntimeId ? !(startingElement := "") && (scope & 1) : 0) : 1) && callback(element) && --index = 0 + return cacheRequest ? element.BuildUpdatedCache(cacheRequest) : element + } + } + throw TargetError("An element matching the condition was not found", -1) +} +static FindCachedElementFromArray(elementArray, condition, index:=1, startingElement:=0) => this.FindElementFromArray(elementArray, condition, index, startingElement,, 1) + +/** + * Clears all highlights created by Element.Highlight + */ +static ClearAllHighlights() => this.IUIAutomationElement.Prototype.Highlight("clearall") + +/** + * Create a property condition from an AHK object + * @param condition Object or Array that contains property conditions. + * * A single property condition consists of an object where the key is the property name, and value is the property value: + * `{Name:"Test"}` => Creates a condition where the Name property must match "Test" exactly + * * Everything inside {} is an "and" condition + * `{Type:"Button", Name:"Something"}` => Name must match "Something" AND Type must be Button + * * Everything inside [] is an "or" condition + * `[{Name:"Test"}, {Name:"Something"}]` Name must match "Test" OR Name must match "Something" + * * Object key "not" creates a not condition + * + * * matchmode key (short form: mm) can be one of UIA.MatchMode values (except StartsWith and RegEx) and defines the MatchMode: + * 2=can contain anywhere in string; 3=exact match + * * casesense key (short form: cs) defines case sensitivity (default: case-sensitive/True): True=case sensitive; False=case insensitive + * + * @param value If this is set then UIA.CreatePropertyCondition will be called instead, with "condition" being the Property name. + * @param flags If `value` was set then this will be passed to UIA.CreatePropertyConditionEx + * + * #### Examples: + * * `CreateCondition({Name:"Test"})` => Name must match "Test" exactly, case-sensitive + * * `CreateCondition({Type:"Button", or:[Name:"Something", Name:"Else"]})` => Name must match "Something" OR "Else", AND Type must be Button + * * `CreateCondition({Name:"Test", cs:0, mm:2})` => Name must contain "Test" and is case-insensitive + * * `CreateCondition("Type", "Button")` => Type must be Button + * + * @returns {UIA.IUIAutomationCondition} + */ +static CreateCondition(condition, value?, flags?) { + local propertyId, match + if IsSet(value) { + propertyId := this.TypeValidation.Property(condition) + if propertyId = 30003 && !IsNumber(value) { + if this.Type.HasOwnProp(value) + value := this.Type.%value% + else if value is String && RegExMatch(value, "\d{5}", &match:="") && Integer(match[]) >= 50000 + value := Integer(match[]) + else + throw ValueError("Type '" value '" in a non-existant type!', -1) + } + if IsSet(flags) && (this.TypeValidation.PropertyConditionFlags(flags)) && this.PropertyVariantTypeBSTR.Has(propertyId) + return this.CreatePropertyConditionEx(propertyId, this.TypeValidation.String(value, "Value"), flags) + return this.CreatePropertyCondition(propertyId, value) + } + return this.__ConditionBuilder(condition) +} + +static __CreateRawCondition(condition) { + local i, nonUIAEncountered + for i in ["index", "i", "scope", "order", "startingElement"] + if condition.HasOwnProp(i) + throw ValueError("This method doesn't support indexes nor named arguments", -2, '"' i '" can be used with FindElement(s) instead') + condition := this.__ConditionBuilder(condition, &nonUIAEncountered:=0) + if nonUIAEncountered & 2 + throw ValueError("This method doesn't support conditions with Cached properties", -2, "Instead use FindElement(s) or FindCachedElement(s)") + else if nonUIAEncountered & 1 + throw ValueError("This method doesn't support conditions with MatchMode RegEx nor StartsWith", -2, "Only MatchModes Exact and SubString are allowed. Use FindElement(s) instead, which supports all MatchModes.") + return condition +} + +static __ConditionBuilder(obj, &nonUIAEncountered?, isParentNotCondition:=0) { + local sanitizeMM, operator, cs, mm, flags, count, k, v, t, i, j, val, match, arr + sanitizeMM := False + if Type(obj) = "Object" { + obj := obj.Clone() ; Speed-testing showed that not cloning (getting props with HasOwnProp and then ignoring them in the loop) is as fast as cloning. + obj.DeleteProp("index"), obj.DeleteProp("i") + operator := obj.DeleteProp("operator") || obj.DeleteProp("op") || "and" + cs := obj.HasOwnProp("casesense") ? obj.casesense : obj.HasOwnProp("cs") ? obj.cs : 1 + obj.DeleteProp("casesense"), obj.DeleteProp("cs") + mm := obj.DeleteProp("matchmode") || obj.DeleteProp("mm") || 3 + if !IsInteger(mm) + mm := this.MatchMode.%mm% + if !IsInteger(cs) + cs := this.CaseSense.%cs% + if IsSet(nonUIAEncountered) { + if (mm = "RegEx" || mm = 1) + nonUIAEncountered |= 1, sanitizeMM := True + } else if !((mm = 3) || (mm = 2)) { ; If creating a "pure" UIA condition, allow only Exact and Substring matchmodes + throw TypeError("MatchMode can only be Exact or Substring (3 or 2) when creating UIA conditions.", -1, "MatchMode 1 and RegEx are allowed with FindElement, FindElements, and ElementFromPath methods.") + } + flags := ((mm = 3 ? 0 : 2) | (!cs)) || obj.DeleteProp("flags") || 0 + count := ObjOwnPropCount(obj), obj := obj.OwnProps() + } else if obj is Array { + operator := "or", flags := 0, count := obj.Length + } else + throw TypeError("Invalid parameter type", -3, Type(obj)) + + if count = 0 + return this.TrueCondition + arr := ComObjArray(0xd, count), i := 0 + for k, v in obj { + if k ~= "i)^Cached" && IsSet(nonUIAEncountered) + nonUIAEncountered |= 2 + try k := IsNumber(k) ? Integer(k) : this.Property.%k% + if IsInteger(k) && k >= 30000 { + switch Type(v), 0 { + case "Array": + if (this.PropertyVariantType[k] & 0x2000) + v := this.AHKArrayToSafeArray(v, this.PropertyVariantType[k] & ~0x2000) + else if !(sanitizeMM && this.PropertyVariantTypeBSTR.Has(k)) || mm = 1 { + ; Creating an "or" condition with same matchmode and casesense + if !v.Length + throw ValueError("Invalid condition: `"or`" condition cannot be empty", -2) + orArr := ComObjArray(0xd, v.Length) + for j, val in v + orArr[j-1] := (mm = 1 ? this.CreateCondition(k, val, !cs | 2) : this.CreateCondition(k, val, flags))[] + t := this.CreateOrConditionFromArray(orArr) + arr[i++] := t[] + continue + } + case "String": + if k = 30000 + v := this.RuntimeIdFromString(v) + else if k = 30003 { + if this.Type.HasOwnProp(v) + v := this.Type.%v% + else if RegExMatch(v, "\d{5}", &match:="") && match[] >= 50000 + v := Integer(match[]) + else + throw ValueError("Type '" v '" in a non-existant type!', -1) + } + case "Integer": + if k = 30003 && v < 50000 + v += 50000 + } + if sanitizeMM && this.PropertyVariantTypeBSTR.Has(k) { + t := mm = 1 ? this.CreateCondition(k, v, !cs | 2) : (isParentNotCondition ? this.CreatePropertyCondition(k, "") : this.CreateNotCondition(this.CreatePropertyCondition(k, ""))) + arr[i++] := t[] + } else if (k >= 30000) { + t := this.CreateCondition(k, v, flags) + arr[i++] := t[] + } + } else if IsObject(v) && !(SubStr(Type(v), 1, 6) = "ComObj") && !v.HasOwnProp("ptr") { + t := this.__ConditionBuilder(v, &nonUIAEncountered, isParentNotCondition ^ (k = "not" || operator = "not")) + if k = "not" || operator = "not" + t := this.CreateNotCondition(t) + arr[i++] := t[] + continue + } else + throw ValueError("Invalid condition encountered: " k, -2) + } + if count = 1 + return operator = "not" ? this.CreateNotCondition(t) : t + switch operator, false { + case "and": + return this.CreateAndConditionFromArray(arr) + case "or": + return this.CreateOrConditionFromArray(arr) + case "not": + return this.CreateNotCondition(this.CreateAndConditionFromArray(arr)) + default: + return this.CreateFalseCondition() + } +} + +/** + * If the element from ElementFromHandle doesn't contain the content of the window, and the window in + * question is a Chromium app (Window Spy shows "ahk_class Chrome_WidgetWin_1"), then this can be used + * to get the element for the content (using Chrome_RenderWidgetHostHWND1 control). + * @param winTitle A window title or other criteria identifying the target window. See AHK WinTitle. + * @param activateChromiumAccessibility Whether to check if the window is a Chromium application + * and if it is then try to activate accessibility. If not 0 then it also defines the timeout period + * for activation. + * @param timeOut How long to wait for confirmation that accessibility is enabled in the app after + * trying to enable accessibility. Default: 500ms + * @param cacheRequest Optional: a cache request object. + * @returns {UIA.IUIAutomationElement} + */ +static ElementFromChromium(winTitle:="", activateChromiumAccessibility:=500, cacheRequest?) { + if activateChromiumAccessibility + return this.ActivateChromiumAccessibility(winTitle, 1, activateChromiumAccessibility, cacheRequest?) + try cHwnd := ControlGetHwnd("Chrome_RenderWidgetHostHWND1", winTitle) + if !IsSet(cHwnd) || !cHwnd + throw TargetError("Chrome_RenderWidgetHostHWND1 content control was not found", -1) + return this.ElementFromHandle(cHwnd, cacheRequest?, False) +} +/** + * Returns whether a window is a Chromium window or not + * @param winTitle A window title or other criteria identifying the target window. See AHK WinTitle. + */ +static WindowIsChromium(winTitle:="") { + try cHwnd := ControlGetHwnd("Chrome_RenderWidgetHostHWND1", winTitle) + return cHwnd ?? 0 +} +/** + * In some setups Chromium-based renderers don't react to UIA calls by enabling accessibility, + * so we need to send the WM_GETOBJECT message to the renderer control to enable accessibility. + * ActivateChromiumAccessibility does that (only once per window) and then returns the content element. + * Explanation why this works: https://www.chromium.org/developers/design-documents/accessibility/#TOC-How-Chrome-detects-the-presence-of-Assistive-Technology + * @param winTitle A window title or other criteria identifying the target window. See AHK WinTitle. + * @param forceActivation By default a window is activated only once per run. If this is set to True + * then the window is activated unconditionally. + * @param timeOutVarRef How long to wait for confirmation that accessibility is enabled in the app after + * trying to enable accessibility. Default: 500ms + * If timeOutVarRef is a VarRef containing a timeout value, then it'll be set to the returned Chromium element. + * @param cacheRequest Optional: a cache request object. + * @returns {UIA.IUIAutomationElement} + * @credit malcev, rommmcek + */ +static ActivateChromiumAccessibility(winTitle:="", forceActivation:=False, timeOutVarRef:=500, cacheRequest?) { + static activatedHwnds := Map(), WM_GETOBJECT := 0x003D + local _, cEl, cHwnd, elVarRef := &cEl, timeOut := timeOutVarRef, hWnd := IsInteger(winTitle) ? winTitle : WinExist(winTitle) + if activatedHwnds.Has(hwnd) && !ForceActivation + return 1 + activatedHwnds[hWnd] := 1, cHwnd := 0 + try cHwnd := ControlGetHwnd("Chrome_RenderWidgetHostHWND1", winTitle) + if !IsSet(cHwnd) || !cHwnd + throw TargetError("No Chromium control (Chrome_RenderWidgetHostHWND1) found", -1) + SendMessage(WM_GETOBJECT := 0x003D, 0, 1,, cHwnd) + if IsObject(timeOutVarRef) && timeOutVarRef is VarRef + elVarRef := timeOutVarRef, timeOut := %timeOutVarRef% + if (%elVarRef% := this.ElementFromHandle(cHwnd, cacheRequest?, False)) { + cEl := cEl ?? %elVarRef% + waitTime := A_TickCount + timeOut + while (A_TickCount < waitTime) { + try { + if cEl.Children.Length + break + } + Sleep 1 + } + } + return %elVarRef% +} + +; Compares two UI Automation elements to determine whether they represent the same underlying UI element. +static CompareElements(el1, el2) { + local areSame + return (ComCall(3, this, "ptr", el1, "ptr", el2, "int*", &areSame := 0), areSame) +} + +; CompareElements sometimes fails to match elements, so this combines CompareElements with checking +; properties Type, Name, ClassName, AutomationId, and BoundingRectangle +static CompareElementsEx(e1, e2) { + local prop, p1, p2, br1, br2 + try { + if UIA.CompareElements(e1, e2) + return 1 + } + for prop in ["Type", "Name", "ClassName", "AutomationId"] { + try p1 := e1.Cached%prop% + catch + p1 := e1.%prop% + try p2 := e2.Cached%prop% + catch + p2 := e2.%prop% + if p1 != p2 + return 0 + } + try br1 := e1.CachedBoundingRectangle + catch + br1 := e1.BoundingRectangle + try br2 := e2.CachedBoundingRectangle + catch + br2 := e2.BoundingRectangle + return br1.l = br2.l && br1.t = br2.t && br1.r = br2.r && br1.b = br2.b +} + +; Compares two integer arrays containing run-time identifiers (IDs) to determine whether their content is the same and they belong to the same UI element. +static CompareRuntimeIds(runtimeId1, runtimeId2) { + local areSame + return (ComCall(4, this, "ptr", runtimeId1, "ptr", runtimeId2, "int*", &areSame := 0), areSame) +} + +/** + * Retrieves the UI Automation element that represents the desktop. + * @param cacheRequest Optional: a cache request object. + * @returns {UIA.IUIAutomationElement} + */ +static GetRootElement(cacheRequest?) { + if IsSet(cacheRequest) + return this.GetRootElementBuildCache(cacheRequest) + if (ComCall(5, this, "ptr*", &root := 0), root) + return this.IUIAutomationElement(root) + throw UnsetError("No element returned by " A_ThisFunc, -1) +} + +/** + * Retrieves a UI Automation element for the specified window. + * @param hwnd WinTitle, window handle, or control handle + * @param cacheRequest Optional: a cache request object. + * @param activateChromiumAccessibility Whether to check if the window is a Chromium application + * and if it is then try to activate accessibility. The value will determine how long to wait for + * activation confirmation. + * If a VarRef is passed, then that variable will be set to the Chromium Document element (only + * if the activation was done). + * @returns {UIA.IUIAutomationElement} + */ +static ElementFromHandle(hwnd:="", cacheRequest?, activateChromiumAccessibility:=500) { + if !IsInteger(hwnd) + hwnd := WinExist(hwnd) + if !hwnd + throw TargetError("No matching window found", -1) + try { + if (activateChromiumAccessibility) + this.ActivateChromiumAccessibility(hwnd,, activateChromiumAccessibility, cacheRequest?) + } + if IsSet(cacheRequest) + return this.ElementFromHandleBuildCache(cacheRequest, hwnd) + + if (ComCall(6, this, "ptr", hwnd, "ptr*", &element := 0), element) + return this.IUIAutomationElement(element) + throw UnsetError("No element returned by " A_ThisFunc, -1) +} +static ElementFromWindow(WinTitle:="", cacheRequest?, activateChromiumAccessibility:=500) => this.ElementFromHandle(WinTitle, cacheRequest?, activateChromiumAccessibility) +/** + * Retrieves the UI Automation element at the specified point on the desktop. + * @param x x coordinate for the screen point. + * Omit both x and y to get the element from the current mouse position. + * @param y y coordinate for the screen point. + * @param cacheRequest Optional: a cache request object. + * @param activateChromiumAccessibility Whether to check if the window is a Chromium application + * and if it is then try to activate accessibility. The value will determine how long to wait for + * activation confirmation. + * If a VarRef is passed, then that variable will be set to the Chromium Document element (only + * if the activation was done). + * @returns {UIA.IUIAutomationElement} + */ +static ElementFromPoint(x?, y?, cacheRequest?, activateChromiumAccessibility:=500) { + local pt64, hwnd + if !(IsSet(x) && IsSet(y)) + DllCall("user32.dll\GetCursorPos", "int64P", &pt64:=0) + else + pt64 := y << 32 | (x & 0xFFFFFFFF) + try { + if (activateChromiumAccessibility && (hwnd := DllCall("GetAncestor", "Ptr", DllCall("user32.dll\WindowFromPoint", "int64", pt64, "ptr"), "UInt", 2, "ptr"))) ; hwnd from point by SKAN + this.ActivateChromiumAccessibility(hwnd,, activateChromiumAccessibility, cacheRequest?) + } + if IsSet(cacheRequest) + return this.ElementFromPointBuildCache(cacheRequest, pt64) + if (ComCall(7, this, "int64", pt64, "ptr*", &element := 0), element) + return this.IUIAutomationElement(element) + throw UnsetError("No element returned by " A_ThisFunc, -1) +} +/** + * Retrieves the smallest UI Automation element at the specified point on the desktop. + * Regular ElementFromPoint doesn't always return the deepest/smallest element. + * @param x x coordinate for the screen point. + * Omit both x and y to get the element from the current mouse position. + * @param y y coordinate for the screen point. + * @param element Optional: optionally provide an element for which the search is performed for. + * This element must be cached with CachedBoundingRectangle and TreeScope.Element+Descendants. + * @param cacheRequest Optional: a cache request object. + * @param deepSearch Whether SmallestElementFromPoint checks all the elements in the element Subtree + * or filters the elements level by level by whether they contain the point or not. Default is false. + * This means that is deepSearch is True then every element in the subtree is checked, but if + * false then only a small part of the tree is checked, which also means a difference in performance. + * DeepSearch might be useful if some of the intermediary elements have an incorrect size (eg in + * Firefox some group elements are 0-sized). + * + * @returns {UIA.IUIAutomationElement} + */ +static SmallestElementFromPoint(x?, y?, element?, cacheRequest?, deepSearch := false) { + static sCacheRequest := this.CreateCacheRequest(["BoundingRectangle"],,5) + local pt64 + + if !(IsSet(x) && IsSet(y)) + DllCall("user32.dll\GetCursorPos", "int64P", &pt64:=0), x := 0xFFFFFFFF & pt64, y := pt64 >> 32 + if deepSearch { + if !IsSet(cacheRequest) + cacheRequest := sCacheRequest + else + cacheRequest.AddProperty(UIA.Property.BoundingRectangle) + + if !IsSet(element) + element := UIA.ElementFromPoint(x, y, sCacheRequest) + return this.CachedSmallestElementInElementContainingPoint(x, y, element) + } + + if !IsSet(element) + element := UIA.ElementFromPoint(x, y) + element := this.SmallestElementInElementContainingPoint(x, y, element) + return IsSet(cacheRequest) ? element.BuildUpdatedCache(cacheRequest) : element +} + +; Returns the smallest element inside another element (it included) that contains the point (x,y) +static SmallestElementInElementContainingPoint(x, y, element) { + static cr := UIA.CreateCacheRequest(["BoundingRectangle"]) + local el, smallest := element, smallestSize := 100000000, rect, evaluated := [element.BuildUpdatedCache(cr)] + while evaluated.Length { + el := evaluated.Pop(), rect := el.CachedBoundingRectangle + if rect.l<=x && rect.r >=x && rect.t <= y && rect.b >= y { + evaluated.Push(el.FindAllBuildCache(cr,, 2)*), size := (rect.r-rect.l)*(rect.b-rect.t) + if size < smallestSize + smallest := el, smallestSize := size + } + } + return smallest +} + +; Requires the elements subtree to be cached with the BoundingRectangle property +static CachedSmallestElementInElementContainingPoint(x, y, element) { + local el, smallest := element, smallestSize := 100000000, rect + for el in element.GetCachedChildren(5) { + rect := el.CachedBoundingRectangle + if rect.l<=x && rect.r >=x && rect.t <= y && rect.b >= y { + size := (rect.r-rect.l)*(rect.b-rect.t) + if size < smallestSize + smallest := el, smallestSize := size + } + } + return smallest +} + +/** + * Retrieves the UI Automation element that has the input focus. + * @param cacheRequest Optional: a cache request object. + * @returns {UIA.IUIAutomationElement} + */ +static GetFocusedElement(cacheRequest?) { + if IsSet(cacheRequest) + return this.GetFocusedElementBuildCache(cacheRequest) + if (ComCall(8, this, "ptr*", &element := 0), element) + return this.IUIAutomationElement(element) + throw UnsetError("No element returned by " A_ThisFunc, -1) +} + +; Retrieves the UI Automation element that has the input focus, prefetches the requested properties and control patterns, and stores the prefetched items in the cache. +; This is a raw wrapper, instead use GetRootElement and specify cacheRequest. +static GetRootElementBuildCache(cacheRequest) { + local root + if (ComCall(9, this, "ptr", this.TypeValidation.CacheRequest(cacheRequest), "ptr*", &root := 0), root) + return this.IUIAutomationElement(root) + throw UnsetError("No element returned by " A_ThisFunc, -1) +} + +; Retrieves a UI Automation element for the specified window, prefetches the requested properties and control patterns, and stores the prefetched items in the cache. +; This is a raw wrapper, instead use ElementFromHandle and specify cacheRequest. +static ElementFromHandleBuildCache(cacheRequest, hwnd) { + local element + if (ComCall(10, this, "ptr", hwnd, "ptr", this.TypeValidation.CacheRequest(cacheRequest), "ptr*", &element := 0), element) + return this.IUIAutomationElement(element) + throw UnsetError("No element returned by " A_ThisFunc, -1) +} + +; Retrieves the UI Automation element at the specified point on the desktop, prefetches the requested properties and control patterns, and stores the prefetched items in the cache. +; This is a raw wrapper, instead use ElementFromPoint and specify cacheRequest. +static ElementFromPointBuildCache(cacheRequest, pt) { + local element + if (ComCall(11, this, "int64", pt, "ptr", this.TypeValidation.CacheRequest(cacheRequest), "ptr*", &element := 0), element) + return this.IUIAutomationElement(element) + throw UnsetError("No element returned by " A_ThisFunc, -1) +} + +; Retrieves the UI Automation element that has the input focus, prefetches the requested properties and control patterns, and stores the prefetched items in the cache. +; This is a raw wrapper, instead use GetFocusedElement and specify cacheRequest. +static GetFocusedElementBuildCache(cacheRequest) { + local element + if (ComCall(12, this, "ptr", this.TypeValidation.CacheRequest(cacheRequest), "ptr*", &element := 0), element) + return this.IUIAutomationElement(element) + throw UnsetError("No element returned by " A_ThisFunc, -1) +} + +; Retrieves a tree walker object that can be used to traverse the Microsoft UI Automation tree. +static CreateTreeWalker(pCondition) { + local walker + if (ComCall(13, this, "ptr", (Type(pCondition) ~= "i)Condition|ComValue") ? pCondition : this.__CreateRawCondition(pCondition), "ptr*", &walker := 0), walker) + return this.IUIAutomationTreeWalker(walker) + throw UnsetError("No TreeWalker returned by " A_ThisFunc, -1) +} + +/** + * Creates a cache request. After obtaining the IUIAutomationCacheRequest interface, use its methods to specify properties and control patterns to be cached when a UI Automation element is obtained. + * @param properties Optional: an array containing properties that will be added to the CacheRequest + * @param patterns Optional: an array containing patterns that will be added to the CacheRequest + * @param scope Optional: set the TreeScope for the CacheRequest (one of UIA.TreeScope values). Default is UIA.TreeScope.Element + * @param mode Optional: set the AutomationElementMode for the CacheRequest (one of UIA.AutomationElementMode values). Default is Full. + * @param filter Optional: a condition for caching elements. Default is ControlView. + */ +static CreateCacheRequest(properties?, patterns?, scope?, mode?, filter?) { + local cacheRequest, v + if cacheRequest := (ComCall(20, this, "ptr*", &cacheRequest := 0), cacheRequest?this.IUIAutomationCacheRequest(cacheRequest):"") { + if IsSet(properties) { + if Type(properties) = "Object" { + if properties.HasOwnProp("filter") + filter := properties.DeleteProp("filter") + if properties.HasOwnProp("mode") + mode := properties.DeleteProp("mode") + if properties.HasOwnProp("scope") + scope := properties.DeleteProp("scope") + if properties.HasOwnProp("patterns") + patterns := properties.DeleteProp("patterns") + if properties.HasOwnProp("properties") + properties := properties.DeleteProp("properties") + } + if properties is Array && properties[1] is String { + for v in properties + cacheRequest.AddProperty(v) + } else if IsObject(properties) { + cacheRequest.AddPropertiesFromCondition(properties) + } else + throw TypeError("Expected properties argument of type Array or Object, but got " Type(properties), -1) + } + if IsSet(patterns) { + if not patterns is Array + throw TypeError("Expected patterns argument of type Array, but got " Type(properties), -1) + for v in patterns + cacheRequest.AddPattern(v) + } + if IsSet(scope) + cacheRequest.TreeScope := scope + if IsSet(mode) + cacheRequest.AutomationElementMode := mode + if IsSet(filter) + cacheRequest.TreeFilter := filter + } + return cacheRequest +} + +static CreateTrueCondition() { + local newCondition + if (ComCall(21, this, "ptr*", &newCondition := 0), newCondition) + return this.IUIAutomationBoolCondition(newCondition) + throw UnsetError("No condition returned by " A_ThisFunc, -1) +} + +; Creates a condition that is always false. +; This method exists only for symmetry with IUIAutomation.CreateTrueCondition(). A false condition will never enable a match with UI Automation elements, and it cannot usefully be combined with any other condition. +static CreateFalseCondition() { + local newCondition + if (ComCall(22, this, "ptr*", &newCondition := 0), newCondition) + return this.IUIAutomationBoolCondition(newCondition) + throw UnsetError("No condition returned by " A_ThisFunc, -1) +} + +; Creates a condition that selects elements that have a property with the specified value. +static CreatePropertyCondition(propertyId, value, variant?) { + local v, newCondition + if A_PtrSize = 4 + v := this.ComVar(value, variant ?? this.PropertyVariantType[propertyId], true), ComCall(23, this, "int", propertyId, "int64", NumGet(v, 'int64'), "int64", NumGet(v, 8, "int64"), "ptr*", &newCondition := 0) + else + ComCall(23, this, "int", propertyId, "ptr", this.ComVar(value, variant ?? this.PropertyVariantType[propertyId], true), "ptr*", &newCondition := 0) + if newCondition + return this.IUIAutomationPropertyCondition(newCondition) + throw Error("Creating property condition failed", -1) +} + +; Creates a condition that selects elements that have a property with the specified value, using optional flags. +; This is a raw wrapper, instead use CreateCondition and specify flags. +static CreatePropertyConditionEx(propertyId, value, flags := 0, variant?) { + local v, newCondition + if A_PtrSize = 4 + v := this.ComVar(value, variant ?? this.PropertyVariantType[propertyId], true), ComCall(24, this, "int", propertyId, "int64", NumGet(v, 'int64'), "int64", NumGet(v, 8, "int64"), "int", flags, "ptr*", &newCondition := 0) + else + ComCall(24, this, "int", propertyId, "ptr", this.ComVar(value, variant ?? this.PropertyVariantType[propertyId], true), "int", flags, "ptr*", &newCondition := 0) + if newCondition + return this.IUIAutomationPropertyCondition(newCondition) + throw Error("Creating property condition failed", -1) +} + +; The Create**Condition** method calls AddRef on each pointers. This means you can call Release on those pointers after the call to Create**Condition** returns without invalidating the pointer returned from Create**Condition**. +; When you call Release on the pointer returned from Create**Condition**, UI Automation calls Release on those pointers. + +; Creates a condition that selects elements that match both of two conditions. +static CreateAndCondition(condition1, condition2) { + local newCondition + if (ComCall(25, this, "ptr", condition1, "ptr", condition2, "ptr*", &newCondition := 0), newCondition) + return this.IUIAutomationAndCondition(newCondition) + throw UnsetError("No condition returned by " A_ThisFunc, -1) +} + +; Creates a condition that selects elements based on multiple conditions, all of which must be true. +static CreateAndConditionFromArray(conditions) { + local newCondition + if (ComCall(26, this, "ptr", conditions is Array ? this.Variant(conditions, 0xd) : conditions, "ptr*", &newCondition := 0), newCondition) + return this.IUIAutomationAndCondition(newCondition) + throw UnsetError("No condition returned by " A_ThisFunc, -1) +} + +; Creates a condition that selects elements based on multiple conditions, all of which must be true. +static CreateAndConditionFromNativeArray(conditions, conditionCount) { + local newCondition + if (ComCall(27, this, "ptr", conditions, "int", conditionCount, "ptr*", &newCondition := 0), newCondition) + return this.IUIAutomationAndCondition(newCondition) + throw UnsetError("No condition returned by " A_ThisFunc, -1) +} + +; Creates a combination of two conditions where a match exists if either of the conditions is true. +static CreateOrCondition(condition1, condition2) { + local newCondition + if (ComCall(28, this, "ptr", condition1, "ptr", condition2, "ptr*", &newCondition := 0), newCondition) + return this.IUIAutomationOrCondition(newCondition) + throw UnsetError("No condition returned by " A_ThisFunc, -1) +} + +; Creates a combination of two or more conditions where a match exists if any of the conditions is true. +static CreateOrConditionFromArray(conditions) { + local newCondition + if (ComCall(29, this, "ptr", conditions is Array ? this.Variant(conditions, 0xd) : conditions, "ptr*", &newCondition := 0), newCondition) + return this.IUIAutomationOrCondition(newCondition) + throw UnsetError("No condition returned by " A_ThisFunc, -1) +} + +; Creates a combination of two or more conditions where a match exists if any one of the conditions is true. +static CreateOrConditionFromNativeArray(conditions, conditionCount) { + local newCondition + if (ComCall(30, this, "ptr", conditions, "ptr", conditionCount, "ptr*", &newCondition := 0), newCondition) + return this.IUIAutomationOrCondition(newCondition) + throw UnsetError("No condition returned by " A_ThisFunc, -1) +} + +; Creates a condition that is the negative of a specified condition. +static CreateNotCondition(condition) { + local newCondition + if (ComCall(31, this, "ptr", condition, "ptr*", &newCondition := 0), newCondition) + return this.IUIAutomationNotCondition(newCondition) + throw UnsetError("No condition returned by " A_ThisFunc, -1) +} + +; Note, Before implementing an event handler, you should be familiar with the threading issues described in Understanding Threading Issues. http,//msdn.microsoft.com/en-us/library/ee671692(v=vs.85).aspx +; A UI Automation client should not use multiple threads to add or remove event handlers. Unexpected behavior can result if one event handler is being added or removed while another is being added or removed in the same client process. +; It is possible for an event to be delivered to an event handler after the handler has been unsubscribed, if the event is received simultaneously with the request to unsubscribe the event. The best practice is to follow the Component Object Model (COM) standard and avoid destroying the event handler object until its reference count has reached zero. Destroying an event handler immediately after unsubscribing for events may result in an access violation if an event is delivered late. + +/** + * Registers a method that handles Microsoft UI Automation events. + * @param handler Handler object from UIA.CreateEventHandler() + * @param element UIA element + * @param eventId One of UIA.Event values + * @param scope Optional TreeScope value: Element, Children, Family (Element+Children), Descendants, Subtree (=Element+Descendants). Default is Descendants. + * @param cacheRequest Optional: cache request object + */ +static AddAutomationEventHandler(handler, element, eventId, scope:=0x4, cacheRequest:=0) => ComCall(32, this, "int", this.TypeValidation.Event(eventId), "ptr", this.TypeValidation.Element(element), "int", this.TypeValidation.TreeScope(scope), "ptr", this.TypeValidation.CacheRequest(cacheRequest), "ptr", handler) + +/** + * Removes the specified UI Automation event handler. + * @param handler Handler object from UIA.CreateEventHandler() + * @param element UIA element + * @param eventId One of UIA.Event values + */ +static RemoveAutomationEventHandler(handler, element, eventId) => ComCall(33, this, "int", this.TypeValidation.Event(eventId), "ptr", this.TypeValidation.Element(element), "ptr", handler) + +; Registers a method that handles property-changed events. +; The UI item specified by element might not support the properties specified by the propertyArray parameter. +; This method serves the same purpose as UIA.AddPropertyChangedEventHandler, but takes a native array of property identifiers instead of a SAFEARRAY. +; Not needed in this library, use UIA.AddPropertyChangedEventHandler instead. +static AddPropertyChangedEventHandlerNativeArray(handler, element, propertyArray, propertyCount, scope:=0x4, cacheRequest:=0) => ComCall(34, this, "ptr", this.TypeValidation.Element(element), "int", this.TypeValidation.TreeScope(scope), "ptr", this.TypeValidation.CacheRequest(cacheRequest), "ptr", handler, "ptr", propertyArray, "int", propertyCount) + +/** + * Registers a method that handles property-changed events. + * The UI item specified by element might not support the properties specified by the propertyArray parameter. + * @param handler Handler object from UIA.CreateEventHandler() + * @param element UIA element + * @param propertyArray An array of UIA.Property values + * @param scope Optional TreeScope value: Element, Children, Family (Element+Children), Descendants, Subtree (=Element+Descendants). Default is Descendants. + * @param cacheRequest Optional: cache request object + */ +static AddPropertyChangedEventHandler(handler, element, propertyArray, scope:=0x4, cacheRequest:=0) { + local i, SafeArray + if !IsObject(propertyArray) + propertyArray := [propertyArray] + SafeArray:=ComObjArray(0x3,propertyArray.Length) + for i, propertyId in propertyArray + SafeArray[i-1]:=propertyId + ComCall(35, this, "ptr", this.TypeValidation.Element(element), "int", this.TypeValidation.TreeScope(scope), "ptr", this.TypeValidation.CacheRequest(cacheRequest), "ptr", handler, "ptr", SafeArray) +} + +; Removes a property-changed event handler. +static RemovePropertyChangedEventHandler(handler, element) => ComCall(36, this, "ptr", this.TypeValidation.Element(element), "ptr", handler) + +/** + * Registers a method that handles structure-changed events. + * @param handler Handler object from UIA.CreateEventHandler() + * @param element UIA element + * @param scope Optional TreeScope value: Element, Children, Family (Element+Children), Descendants, Subtree (=Element+Descendants). Default is Descendants. + * @param cacheRequest Optional: cache request object + */ +static AddStructureChangedEventHandler(handler, element, scope:=0x4, cacheRequest:=0) => ComCall(37, this, "ptr", this.TypeValidation.Element(element), "int", this.TypeValidation.TreeScope(scope), "ptr", this.TypeValidation.CacheRequest(cacheRequest), "ptr", handler) + +; Removes a structure-changed event handler. +static RemoveStructureChangedEventHandler(handler, element) => ComCall(38, this, "ptr", this.TypeValidation.Element(element), "ptr", handler) + +/** + * Registers a method that handles focus-changed events. + * Focus-changed events are system-wide; you cannot set a narrower scope. + * @param handler Handler object from UIA.CreateEventHandler() + * @param cacheRequest Optional: cache request object + */ +static AddFocusChangedEventHandler(handler, cacheRequest:=0) => ComCall(39, this, "ptr", this.TypeValidation.CacheRequest(cacheRequest), "ptr", handler) + +; Removes a focus-changed event handler. +static RemoveFocusChangedEventHandler(handler) => ComCall(40, this, "ptr", handler) + +; Removes all registered Microsoft UI Automation event handlers. +static RemoveAllEventHandlers() => ComCall(41, this) + +; Converts an array of integers to a SAFEARRAY. +static IntNativeArrayToSafeArray(array, arrayCount) { + local safeArray + return (ComCall(42, this, "ptr", array, "int", arrayCount, "ptr*", &safeArray := 0), ComValue(0x2003, safeArray)) +} + +; Converts a SAFEARRAY of integers to an array. +static IntSafeArrayToNativeArray(intArray) { + local array, arrayCount + return (ComCall(43, this, "ptr", intArray, "ptr*", &array := 0, "int*", &arrayCount := 0), this.NativeArray(array, arrayCount, "int")) +} +; Creates a VARIANT that contains the coordinates of a rectangle. +; The returned VARIANT has a data type of VT_ARRAY | VT_R8. +static RectToVariant(rc) { + local var + return (ComCall(44, this, "ptr", rc, "ptr", var := this.ComVar()), var) +} + +; Converts a VARIANT containing rectangle coordinates to a RECT. +static VariantToRect(var) { + if A_PtrSize = 4 + ComCall(45, this, "int64", NumGet(var, "int64"), "int64", NumGet(var, 8, "int64"), "ptr", rc := this.NativeArray(0, 4, "Int")) + else + ComCall(45, this, "ptr", var, "ptr", rc := this.NativeArray(0, 4, "Int")) + return rc +} + +; Converts a SAFEARRAY containing rectangle coordinates to an array of type RECT. +static SafeArrayToRectNativeArray(rects) { + local rectArrayCount + return (ComCall(46, this, "ptr", rects, "ptr*", &rectArray := 0, "int*", &rectArrayCount := 0), this.NativeArray(rectArray, rectArrayCount, "int")) +} + +; Creates a instance of a proxy factory object. +; Use the IUIAutomationProxyFactoryMapping interface to enter the proxy factory into the table of available proxies. +static CreateProxyFactoryEntry(factory) { + local factoryEntry + if (ComCall(47, this, "ptr", factory, "ptr*", &factoryEntry := 0), factoryEntry) + return this.IUIAutomationProxyFactoryEntry(factoryEntry) + throw UnsetError("No ProxyFactoryEntry returned by " A_ThisFunc, -1) +} + +; Retrieves an object that represents the mapping of Window classnames and associated data to individual proxy factories. This property is read-only. +static ProxyFactoryMapping() { + local factoryMapping + if (ComCall(48, this, "ptr*", &factoryMapping := 0), factoryMapping) + return this.IUIAutomationProxyFactoryMapping(factoryMapping) + throw UnsetError("No ProxyFactoryMapping returned by " A_ThisFunc, -1) +} + +; The programmatic name is intended for debugging and diagnostic purposes only. The string is not localized. +; This property should not be used in string comparisons. To determine whether two properties are the same, compare the property identifiers directly. + +; Retrieves the registered programmatic name of a property. +static GetPropertyProgrammaticName(property) { + local name + return (ComCall(49, this, "int", property, "ptr*", &name := 0), this.BSTR(name)) +} + +; Retrieves the registered programmatic name of a control pattern. +static GetPatternProgrammaticName(pattern) { + local name + return (ComCall(50, this, "int", this.TypeValidation.Pattern(pattern), "ptr*", &name := 0), this.BSTR(name)) +} + +; This method is intended only for use by Microsoft UI Automation tools that need to scan for properties. It is not intended to be used by UI Automation clients. +; There is no guarantee that the element will support any particular control pattern when asked for it later. + +; Retrieves the control patterns that might be supported on a UI Automation element. +static PollForPotentialSupportedPatterns(pElement, &patternIds, &patternNames) { + ComCall(51, this, "ptr", this.TypeValidation.Element(pElement), "ptr*", &patternIds := 0, "ptr*", &patternNames := 0) + patternIds := this.SafeArrayToAHKArray(patternIds, 3), patternNames := this.SafeArrayToAHKArray(patternNames, 8) +} + +; Retrieves the properties that might be supported on a UI Automation element. +static PollForPotentialSupportedProperties(pElement, &propertyIds, &propertyNames) { + ComCall(52, this, "ptr", this.TypeValidation.Element(pElement), "ptr*", &propertyIds := 0, "ptr*", &propertyNames := 0) + propertyIds := this.SafeArrayToAHKArray(propertyIds, 3), propertyNames := this.SafeArrayToAHKArray(propertyNames, 8) +} + +; Checks a provided VARIANT to see if it contains the Not Supported identifier. +; After retrieving a property for a UI Automation element, call this method to determine whether the element supports the retrieved property. CheckNotSupported is typically called after calling a property retrieving method such as GetPropertyValue. +static CheckNotSupported(value) { + if A_PtrSize = 4 + value := this.ComVar(value, , true), ComCall(53, this, "int64", NumGet(value, "int64"), "int64", NumGet(value, 8, "int64"), "int*", &isNotSupported := 0) + else + ComCall(53, this, "ptr", this.ComVar(value, , true), "int*", &isNotSupported := 0) + return isNotSupported +} + + +; This method enables UI Automation clients to get UIA.IUIAutomationElement interfaces for accessible objects implemented by a Microsoft Active Accessiblity server. +; This method may fail if the server implements UI Automation provider interfaces alongside Microsoft Active Accessibility support. +; The method returns E_INVALIDARG if the underlying implementation of the Microsoft UI Automation element is not a native Microsoft Active Accessibility server; that is, if a client attempts to retrieve the IAccessible interface for an element originally supported by a proxy object from Oleacc.dll, or by the UIA-to-MSAA Bridge. + +; Retrieves a UI Automation element for the specified accessible object from a Microsoft Active Accessibility server. +; Currently not working +static ElementFromIAccessible(accessible, childId:=0) => (InStr(Type(accessible), "IAccessible") ? (ComCall(56, this, "ptr", accessible, "int", accessible.childId, "ptr*", &element := 0), this.IUIAutomationElement(element)) : (ComCall(56, this, "ptr", ComObjValue(accessible), "int", childId, "ptr*", &element := 0), this.IUIAutomationElement(element))) + +; Retrieves a UI Automation element for the specified accessible object from a Microsoft Active Accessibility server, prefetches the requested properties and control patterns, and stores the prefetched items in the cache. +static ElementFromIAccessibleBuildCache(cacheRequest, accessible, childId:=0) => (InStr(Type(accessible), "IAccessible") ? (ComCall(57, this, "ptr", accessible.accessible, "int", accessible.childId, "ptr", this.TypeValidation.CacheRequest(cacheRequest), "ptr*", &element := 0), this.IUIAutomationElement(element)) : (ComCall(57, this, "ptr", accessible, "int", childId, "ptr", this.TypeValidation.CacheRequest(cacheRequest), "ptr*", &element := 0), this.IUIAutomationElement(element))) + +; ---------- IUIAutomation3 ---------- + +/** + * Registers a method that handles programmatic text-edit events. + * @param handler Handler object from UIA.CreateEventHandler() + * @param element UIA element + * @param textEditChangeType The specific change type to listen for, one of UIA.TextEditChangeType values (None, AutoCorrect, Composition, CompositionFinalized, AutoComplete) + * @param scope Optional TreeScope value: Element, Children, Family (Element+Children), Descendants, Subtree (=Element+Descendants). Default is Descendants. + * @param cacheRequest Optional: cache request object + */ +static AddTextEditTextChangedEventHandler(handler, element, textEditChangeType, scope:=0x4, cacheRequest:=0) => (ComCall(64, this, "ptr", UIA.TypeValidation.Element(element), "int", UIA.TypeValidation.TreeScope(scope), "int", UIA.TypeValidation.TextEditChangeType(textEditChangeType), "ptr", UIA.TypeValidation.CacheRequest(cacheRequest), "ptr", handler)) +static RemoveTextEditTextChangedEventHandler(handler, element) => ComCall(65, this, "ptr", UIA.TypeValidation.Element(element), "ptr", handler) + +; ---------- IUIAutomation4 ---------- + +/** + * Registers a method that handles change events. + * @param handler Handler object from UIA.CreateEventHandler() + * @param element UIA element + * @param changeTypes An array of UIA.Changes values that indicate the change types the event represents. + * @param scope Optional TreeScope value: Element, Children, Family (Element+Children), Descendants, Subtree (=Element+Descendants). Default is Descendants. + * @param cacheRequest Optional: cache request object + */ +static AddChangesEventHandler(handler, element, changeTypes, scope:=0x4, cacheRequest:=0) { + local k, v + if !IsObject(changeTypes) + changeTypes := [changeTypes] + nativeArray := this.NativeArray(0, changeTypes.Length, "int") + for k, v in changeTypes + NumPut("int", v, nativeArray, (k-1)*4) + ComCall(66, this, "ptr", this.TypeValidation.Element(element), "int", this.TypeValidation.TreeScope(scope), "ptr", nativeArray, "int", changeTypes.Length, "ptr", this.TypeValidation.CacheRequest(cacheRequest), "ptr", handler) +} +static RemoveChangesEventHandler(handler, element) => (ComCall(67, this, "ptr", this.TypeValidation.Element(element), "ptr", handler)) + +; ---------- IUIAutomation5 ---------- + +/** + * Registers a method that handles notification events. + * @param handler Handler object from UIA.CreateEventHandler() + * @param element UIA element + * @param scope Optional TreeScope value: Element, Children, Family (Element+Children), Descendants, Subtree (=Element+Descendants). Default is Descendants. + * @param cacheRequest Optional: cache request object + */ +static AddNotificationEventHandler(handler, element, scope:=0x4, cacheRequest:=0) => (ComCall(68, this, "ptr", this.TypeValidation.Element(element), "uint", this.TypeValidation.TreeScope(scope), "ptr", this.TypeValidation.CacheRequest(cacheRequest), "ptr", handler)) +static RemoveNotificationEventHandler(handler, element) => (ComCall(69, this, "ptr", this.TypeValidation.Element(element), "ptr", handler)) + +; ---------- IUIAutomation6 ---------- + +; Registers one or more event listeners in a single method call. +static CreateEventHandlerGroup() { + local out + return (ComCall(70, this, "ptr*", &out:=0), this.IUIAutomationEventHandlerGroup(out)) +} +; Registers a collection of event handler methods specified with the IUIAutomation6 CreateEventHandlerGroup. +static AddEventHandlerGroup(handlerGroup, element) => (ComCall(71, this, "ptr", this.TypeValidation.Element(element), "ptr", handlerGroup)) +static RemoveEventHandlerGroup(handlerGroup, element) => (ComCall(72, this, "ptr", this.TypeValidation.Element(element), "ptr", handlerGroup)) +; Registers a method that handles when the active text position changes. +static AddActiveTextPositionChangedEventHandler(handler, element, scope:=0x4, cacheRequest:=0) => (ComCall(77, this, "ptr", this.TypeValidation.Element(element), "int", this.TypeValidation.TreeScope(scope), "ptr", this.TypeValidation.CacheRequest(cacheRequest), "ptr", handler)) +static RemoveActiveTextPositionChangedEventHandler(handler, element) => (ComCall(78, this, "ptr", this.TypeValidation.Element(element), "ptr", handler)) + +; ---------- IUIAutomation7 ---------- +; Has no properties/methods + + +; ---------- Internal methods and properties ---------- + +static SetScreenReader(state, fWinIni:=2) { + DllCall("user32.dll\SystemParametersInfo", "uint", 0x0047, "uint", state, "uint", 0, "int", fWinIni) ; SPI_SETSCREENREADER +} +static GetScreenReader() { + (A_PtrSize = 4) ? DllCall("user32.dll\SystemParametersInfo", "uint", 0x0046, "uint", 0, "ptr*", &screenreader:=0, "uint", 0) : DllCall("user32.dll\SystemParametersInfo", "uint", 0x0046, "uint", 0, "ptr*", &screenreader:=0) ; SPI_GETSCREENREADER + return screenreader +} + +; BSTR wrapper, convert BSTR to AHK string and free it +static BSTR(ptr) { + static _ := DllCall("LoadLibrary", "str", "oleaut32.dll") + if ptr { + s := StrGet(ptr), DllCall("oleaut32\SysFreeString", "ptr", ptr) + return s + } +} +static SafeArrayToAHKArray(sArr, varType:=3) { + local _, v, ret := [] + if IsInteger(sArr) + sArr := ComValue(0x2000 | varType, sArr) + for _, v in sArr + ret.Push(v) + ret.DefineProp("__Value", {value:sArr}) + ret.DefineProp("ptr", {value:sArr.ptr}) + return ret +} +static AHKArrayToSafeArray(arr, varType:=3) { + local index, value, cArr + cArr := ComObjArray(varType, arr.Length) + for index, value in arr + cArr[index-1] := value + return cArr +} +; X can be pt64 as well, in which case Y should be omitted +static WindowFromPoint(X, Y?) { ; by SKAN and Linear Spoon + return DllCall("GetAncestor", "Ptr", DllCall("user32.dll\WindowFromPoint", "Int64", IsSet(Y) ? (Y << 32 | (X & 0xFFFFFFFF)) : X), "UInt", 2, "Ptr") +} + +/** + * Checks whether two rectangles intersect and if they do, then returns an object containing the + * rectangle of the intersection: {l:left, t:top, r:right, b:bottom} + * Note 1: Overlapping area must be at least 1 unit. + * Note 2: Second rectangle starting at the edge of the first doesn't count as intersecting: + * {l:100, t:100, r:200, b:200} does not intersect {l:200, t:100, 400, 400} + * @param l1 x-coordinate of the upper-left corner of the first rectangle + * @param t1 y-coordinate of the upper-left corner of the first rectangle + * @param r1 x-coordinate of the lower-right corner of the first rectangle + * @param b1 y-coordinate of the lower-right corner of the first rectangle + * @param l2 x-coordinate of the upper-left corner of the second rectangle + * @param t2 y-coordinate of the upper-left corner of the second rectangle + * @param r2 x-coordinate of the lower-right corner of the second rectangle + * @param b2 y-coordinate of the lower-right corner of the second rectangle + * @returns {Object} + */ +static IntersectRect(l1, t1, r1, b1, l2, t2, r2, b2) { + rect1 := Buffer(16), rect2 := Buffer(16), rectOut := Buffer(16) + NumPut("int", l1, "int", t1, "int", r1, "int", b1, rect1) + NumPut("int", l2, "int", t2, "int", r2, "int", b2, rect2) + if DllCall("user32\IntersectRect", "Ptr", rectOut, "Ptr", rect1, "Ptr", rect2) + return {l:NumGet(rectOut, 0, "Int"), t:NumGet(rectOut, 4, "Int"), r:NumGet(rectOut, 8, "Int"), b:NumGet(rectOut, 12, "Int")} +} + +; Creates a variant to pass into ComCalls +class Variant { + __New(Value := unset, VarType := 0xC) { + local v, i, obj + static SIZEOF_VARIANT := 8 + (2 * A_PtrSize) + this.var := Buffer(SIZEOF_VARIANT, 0) + if VarType != 0xC + NumPut "ushort", VarType, this.var + this.owner := True + if IsSet(Value) { + if (Type(Value) == "ComVar") { + this.var := Value.var, this.ref := Value.ref, this.obj := Value, this.owner := False + return + } + if (IsObject(Value)) { + this.ref := ComValue(0x400C, this.var.ptr) + if Value is Array { + if Value.Length { + switch Type(Value[1]) { + case "Integer": VarType := 3 + case "String": VarType := 8 + case "Float": VarType := 5 + case "ComValue", "ComObject": VarType := ComObjType(Value[1]) + default: VarType := 0xC + } + } else + VarType := 0xC + ComObjFlags(obj := ComObjArray(VarType, Value.Length), -1), i := 0, this.ref[] := obj + for v in Value + obj[i++] := v + return + } + } + } + this.ref := ComValue(0x4000 | VarType, this.var.Ptr + (VarType = 0xC ? 0 : 8)) + this.ref[] := Value + } + __Delete() => (this.owner ? DllCall("oleaut32\VariantClear", "ptr", this.var) : 0) + __Item { + get => this.Type=0xB?-this.ref[]:this.ref[] + set => this.ref[] := this.Type=0xB?(!value?0:-1):value + } + Ptr => this.var.Ptr + Size => this.var.Size + Type { + get => NumGet(this.var, "ushort") + set { + if (!this.IsVariant) + throw PropertyError("VarType is not VT_VARIANT, Type is read-only.", -2) + NumPut("ushort", Value, this.var) + } + } + IsVariant => ComObjType(this.ref) & 0xC +} + +; Construction and deconstruction VARIANT struct +class ComVar { + /** + * Construction VARIANT struct, `ptr` property points to the address, `__Item` property returns var's Value + * @param vVal Values that need to be wrapped, supports String, Integer, Double, Array, ComValue, ComObjArray + * ### Examples + * `var1 := ComVar('string'), MsgBox(var1[])` + * + * `var2 := ComVar([1,2,3,4], , true)` + * + * `var3 := ComVar(ComValue(0xb, -1))` + * @param vType Variant's type, VT_VARIANT (default) + * @param convert Convert AHK's array to ComObjArray + */ + __New(vVal := unset, vType := 0xC, convert := false) { + local v, i, obj + static size := 8 + 2 * A_PtrSize + this.var := Buffer(size, 0), this.owner := true + this.ref := ComValue(0x4000 | vType, this.var.Ptr + (vType = 0xC ? 0 : 8)) + if vType != 0xC + NumPut "ushort", vType, this.var + if IsSet(vVal) { + if (Type(vVal) == "ComVar") { + this.var := vVal.var, this.ref := vVal.ref, this.obj := vVal, this.owner := false + } else { + if (IsObject(vVal)) { + if (vType != 0xC) + this.ref := ComValue(0x400C, this.var.ptr) + if convert && (vVal is Array) { + switch Type(vVal[1]) { + case "Integer": vType := 3 + case "String": vType := 8 + case "Float": vType := 5 + case "ComValue", "ComObject": vType := ComObjType(vVal[1]) + default: vType := 0xC + } + ComObjFlags(obj := ComObjArray(vType, vVal.Length), -1), i := 0, this.ref[] := obj + for v in vVal + obj[i++] := v + } else + this.ref[] := vVal + } else + this.ref[] := vVal + } + } + } + __Delete() => (this.owner && this.var ? DllCall("oleaut32\VariantClear", "ptr", this.var) : 0) + __Item { + get => this.ref[] + set => this.ref[] := value + } + Ptr => this.var.Ptr + Size => this.var.Size + Type { + get => NumGet(this.var, "ushort") + set { + if (!this.IsVariant) + throw PropertyError("VarType is not VT_VARIANT, Type is read-only.", -2) + NumPut("ushort", Value, this.var) + } + } + IsVariant => ComObjType(this.ref) & 0xC +} +; NativeArray is C style array, zero-based index, it has `__Item` and `__Enum` property +class NativeArray { + __New(ptr, count, type := "ptr") { + static _ := DllCall("LoadLibrary", "str", "ole32.dll") + static bits := { UInt: 4, UInt64: 8, Int: 4, Int64: 8, Short: 2, UShort: 2, Char: 1, UChar: 1, Double: 8, Float: 4, Ptr: A_PtrSize, UPtr: A_PtrSize } + this.size := (this.count := count) * (bit := bits.%type%), this.ptr := ptr || DllCall("ole32\CoTaskMemAlloc", "uint", this.size, "ptr") + this.DefineProp("__Item", { get: (s, i) => NumGet(s, i * bit, type) }) + this.DefineProp("__Enum", { call: (s, i) => (i = 1 ? + (i := 0, (&v) => i < count ? (v := NumGet(s, i * bit, type), ++i) : false) : + (i := 0, (&k, &v) => (i < count ? (k := i, v := NumGet(s, i * bit, type), ++i) : false)) + ) }) + } + __Delete() => DllCall("ole32\CoTaskMemFree", "ptr", this) +} + +class TypeValidation { + static __Call(method, args) { + local val + if UIA.HasProp(method) { ; Handles enumeration type validation: UIA.TypeValidation.TreeScope(scope) + val := args[1] + if IsInteger(val) { + try UIA.%method%[val] + catch + throw ValueError("UIA." method " does not contain constant `"" val "`"", -2) + return val is Integer ? val : Integer(val) + } + if val is String { + try return UIA.%method%.%val% + throw ValueError("UIA." method " does not contain value for `"" val "`"", -2) + } + throw TypeError(method " requires parameter with type Integer or String, but received " Type(val), -2) + } else { + if !(val := args[1]) + return val + if (IsObject(val) && (val is UIA.IUIAutomation%method%)) || (val is ComValue) || (val is Integer) + return val + throw TypeError(method " requires parameter with type UIA.IUIAutomation" method ", but was provided type " Type(val), -2) + } + } + static Integer(arg, paramName) { + if IsInteger(arg) + return arg is Integer ? arg : Integer(arg) + throw TypeError(paramName " requires type Integer, but received type " Type(arg), -2) + } + static String(arg, paramName) { + if arg is String + return arg + if !(arg is Object) + return String(arg) + throw TypeError(paramName " requires type String, but received type " Type(arg), -2) + } + static Object(arg, paramName) { + if arg is Object + return arg + throw TypeError(paramName " requires type Object, but received type " Type(arg), -2) + } + ; Provide pre-optimized methods for commonly used validations + static RawCondition(arg) { + if (Type(arg) ~= "i)Condition|ComValue") + return arg + throw TypeError("Condition argument requires parameter with type UIA.IUIAutomationCondition or a ComValue, but receiver " Type(arg), -2) + } + static CacheRequest(arg) { + if !arg || (arg is UIA.IUIAutomationCacheRequest) || (arg is ComValue) || (arg is Integer) + return arg + throw TypeError("CacheRequest argument requires parameter with type UIA.IUIAutomationCacheRequest, but received " Type(arg), -2) + } + static Element(arg) { + if !arg || (arg is UIA.IUIAutomationElement) || (arg is ComValue) || (arg is Integer) + return arg + throw TypeError("Element argument requires parameter with type UIA.IUIAutomationElement, but received " Type(arg), -2) + } + static TreeScope(arg) { + if IsInteger(arg) { + if arg < 1 || arg > 31 + throw ValueError("UIA.TreeScope does not contain constant `"" arg "`"", -2) + return arg is Integer ? arg : Integer(arg) + } else if arg is String { + try return UIA.TreeScope.%arg% + throw ValueError("UIA.TreeScope does not contain value for `"" arg "`"", -2) + } + throw TypeError("TreeScope requires parameter with type Integer or String, but received " Type(arg), -2) + } + static Type(arg) { + if IsInteger(arg) { + if arg < 50 && arg >= 0 + return arg + 50000 + if arg < 50000 || arg > 50050 + throw ValueError("UIA.Type does not contain constant `"" arg "`"", -2) + return Integer(arg) + } else if arg is String { + try return UIA.Type.%arg% + try { + local match + if RegExMatch(arg, "\d{5}", &match:="") && Integer(match[]) >= 50000 + return Integer(match[]) + } + throw ValueError("UIA.Type does not contain value for `"" arg "`"", -2) + } + throw TypeError("UIA.Type requires parameter with type Integer or String, but received " Type(arg), -2) + } + static Property(arg) { + if IsInteger(arg) { + try UIA.Property[arg] + catch + throw ValueError("UIA.Property does not contain constant `"" arg "`"", -2) + return Integer(arg) + } else if arg is String { + try return UIA.Property.%RegexReplace(arg, "^(Cached|Current)",,,1)% + throw ValueError("UIA.Property does not contain value for `"" arg "`"", -2) + } + throw TypeError("Property requires parameter with type Integer or String, but received " Type(arg), -2) + } + static Pattern(arg) { + if IsInteger(arg) { + try UIA.Pattern[arg] + catch + throw ValueError("UIA.Pattern does not contain constant `"" arg "`"", -2) + return Integer(arg) + } else if arg is String { + try return UIA.Pattern.%StrReplace(arg, "Pattern")% + throw ValueError("UIA.Pattern does not contain value for `"" arg "`"", -2) + } + throw TypeError("Pattern requires parameter with type Integer or String, but received " Type(arg), -2) + } + static TreeTraversalOptions(arg) { + if IsInteger(arg) { + try UIA.TreeTraversalOptions[arg] + catch + throw ValueError("UIA.TreeTraversalOptions does not contain constant `"" arg "`"", -2) + return Integer(arg) + } else if arg is String { + try return UIA.TreeTraversalOptions.%arg% + try return UIA.TreeTraversalOptions.%arg "Order"% + throw ValueError("UIA.TreeTraversalOptions does not contain value for `"" arg "`"", -2) + } + throw TypeError("Invalid type provided for UIA.TreeTraversalOptions", -2, "Allowed types are Integer and String, but was provided type " Type(arg)) + } +} + +; The base class for IUIAutomation objects that return releasable pointers +class IUIAutomationBase { + __New(ptr) { + if !ptr + throw ValueError('Invalid IUnknown interface pointer', -2, this.__Class) + this.DefineProp("ptr", {Value:ptr}) + } + __Delete() => this.Release() + __Item => (ObjAddRef(this.ptr), ComValue(0xd, this.ptr)) + AddRef() => ObjAddRef(this.ptr) + Release() => this.ptr ? ObjRelease(this.ptr) : 0 + + __Get(Name, Params) { + if this.base.HasOwnProp(NewName := StrReplace(Name, "Current")) + return this.%NewName% + throw PropertyError("Property " Name " not found in " this.__Class " Class.",-2,Name) + } + + __Call(Name, Params) { + if this.base.HasOwnProp(NewName := StrReplace(Name, "Current")) + return this.%NewName%(Params*) + throw MethodError("Method " Name " not found in " this.__Class " Class.",-2,Name) + } +} +/* + Exposes methods and properties for a UI Automation element, which represents a UI item. + Microsoft documentation: https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nn-uiautomationclient-iuiautomationelement +*/ +class IUIAutomationElement extends UIA.IUIAutomationBase { + /** + * Enables array-like use of UIA elements to access child elements. + * If value is an integer then the nth corresponding child will be returned. + * A negative integer return the nth last child + * Eg. Element[2] ==> returns second child of Element + * Element[-1] ==> returns the last child of Element + * If value is a string, then it will be parsed either as an UIA path that was generated by + * UIAViewer, or an comma-separated path of integers/Types + * If value is an object, then it will be used in a FindElement call with scope set to Children. + * Eg. Element[{Type:"Button"}] will return the first child with Type Button. + * @returns {UIA.IUIAutomationElement} + */ + __Item[params*] { + get { + local el, _, param, i, arr, found, path, m + el := this + for _, param in params { + if IsObject(param) { + try el := el.FindElement(param, 2) + catch TargetError { + el := "" + } catch { + try el := el.FindCachedElement(param, 2) + catch + el := "" + } + } else if param is Integer { + try { + ComCall(6, el, "int", 2, "ptr", UIA.TrueCondition, "ptr*", &found := 0) + if found { + arr := UIA.IUIAutomationElementArray(found) + if (i := param < 0 ? arr.Length+param : param-1) <= arr.Length + el := arr.GetElement(i) + else + el := "" + } + } catch { + try { + arr := el.CachedChildren + el := arr[param < 0 ? arr.Length+param+1 : param] + } catch + el := "" + } + } else if param is String { + maybeEl := "" + for path in StrSplit(param, "|") { + try { + if InStr(path, ".") || InStr(path, ",") { + arr := StrSplit(StrReplace(path, ".", ","), ",") + for i, int in arr + arr[i] := IsInteger(int) ? Integer(int) : int + maybeEl := el[arr*] + } else if RegexMatch(path, "i)([a-zA-Z]+) *(\d+)?", &m:="") && UIA.Type.HasOwnProp(m[1]) { + try maybeEl := el.FindElement({Type:m[1], i:(m.Count > 1 ? m[2] : 1)}, 2) + catch TargetError + maybeEl := "" + catch + try maybeEl := el.FindCachedElement({Type:m[1], i:m[2]}, 2) + } else if !(path ~= ",\.+-") + maybeEl := el[UIA.DecodePath(path)*] + else + continue + break ; no errors encountered means that match was found + } + } + el := maybeEl + } else + throw TypeError("Invalid item type at index " _, -1) + if !el + throw IndexError("Invalid index/condition at index " _, -1) + } + return el + } + } + /** + * Enables enumeration of UIA elements, usually in a for loop. + * Usage: + * for [index, ] child in Element + */ + __Enum(varCount) { + local maxLen := this.Length, i := 0, children := this.GetChildren() + EnumElements(&element) { + if ++i > maxLen + return false + element := children[i] + return true + } + EnumIndexAndElements(&index, &element) { + if ++i > maxLen + return false + index := i + element := children[i] + return true + } + return (varCount = 1) ? EnumElements : EnumIndexAndElements + } + /** + * Getter for element properties and element supported pattern properties. + * This allows for syntax such as: + * Element.Name == Element.CurrentName + * Element.ValuePattern == Element.GetPattern("Value") + */ + __Get(Name, Params) { + local pName, pVal, NewName := StrReplace(Name, "Current",,,,1), err, tryName + if (SubStr(Name, 1, 6) = "Cached") { + if UIA.Property.HasOwnProp(PropName := SubStr(Name, 7)) + return this.GetCachedPropertyValue(UIA.Property.%PropName%) + } else if this.base.HasOwnProp(NewName) + return this.%NewName% + else if UIA.Property.HasOwnProp(NewName) + return this.GetPropertyValue(UIA.Property.%NewName%) + else { + for pName, pVal in UIA.Pattern.OwnProps() + if IsInteger(pVal) && IsAlnum(pName) && UIA.HasProp("IUIAutomation" pName "Pattern") && (pName != "LegacyIAccessible") { ; Skip LegacyIAccessible to avoid name collisions (eg Select) + if tryName := UIA.IUIAutomation%pName%Pattern.Prototype.HasProp(NewName) ? NewName : UIA.IUIAutomation%pName%Pattern.Prototype.HasProp(Name) ? Name : "" { + try return this.%pName%Pattern.%tryName% + catch Any as err { + if this.Is%pName%PatternAvailable + throw %Type(err)%(err.Message, -1, err.Extra) + } + } + } + ; Since LegacyIAccessible was skipped, then try LegacyIAccessible also + if (tryName := UIA.IUIAutomationLegacyIAccessiblePattern.Prototype.HasProp(NewName) ? NewName : UIA.IUIAutomationLegacyIAccessiblePattern.Prototype.HasProp(Name) ? Name : "") && this.IsLegacyIAccessiblePatternAvailable + return this.LegacyIAccessiblePattern.%tryName% + throw PropertyError("Property " Name " not found in " this.__Class " Class.",-1,Name) + } + } + ; Setter for UIA element and pattern properties. + __Set(Name, Params, Value) { + local pVal, pName, NewName := StrReplace(Name, "Current",,,,1), tryName, err + if this.base.HasOwnProp(NewName) + return this.%NewName% := Value + for pName, pVal in UIA.Pattern.OwnProps() + if IsInteger(pVal) && IsAlnum(pName) && UIA.HasProp("IUIAutomation" pName "Pattern") && pName != "LegacyIAccessible" { + if tryName := UIA.IUIAutomation%pName%Pattern.Prototype.HasProp(NewName) ? NewName : UIA.IUIAutomation%pName%Pattern.Prototype.HasProp(Name) ? Name : "" { + try return this.%pName%Pattern.%tryName% := Value + catch Any as err { + if this.Is%pName%PatternAvailable + throw %Type(err)%(err.Message, -1, err.Extra) + } + } + } + ; Since LegacyIAccessible was skipped, then try LegacyIAccessible also + if (tryName := UIA.IUIAutomationLegacyIAccessiblePattern.Prototype.HasProp(NewName) ? NewName : UIA.IUIAutomationLegacyIAccessiblePattern.Prototype.HasProp(Name) ? Name : "") && this.IsLegacyIAccessiblePatternAvailable + return this.LegacyIAccessiblePattern.%tryName% := Value + throw PropertyError("This class does not support adding properties") + } + /** + * Meta-function for calling methods from supperted patterns. + * This allows for syntax such as: + * Element.Invoke() == Element.GetPattern("Value").Invoke() + */ + __Call(Name, Params) { + local NewName, pName, pVal, tryName, err + if this.base.HasOwnProp(NewName := StrReplace(Name, "Current",,,,1)) + return this.%NewName%(Params*) + for pName, pVal in UIA.Pattern.OwnProps() + if IsInteger(pVal) && IsAlpha(pName) && UIA.HasProp("IUIAutomation" pName "Pattern") && (pName != "LegacyIAccessible") { + if (tryName := UIA.IUIAutomation%pName%Pattern.Prototype.HasMethod(Name) ? Name : UIA.IUIAutomation%pName%Pattern.Prototype.HasMethod(NewName) ? NewName : "") { + try return this.%pName%Pattern.%tryName%(Params*) + catch Any as err { + if this.Is%pName%PatternAvailable + throw %Type(err)%(err.Message, -1, err.Extra) + } + } + } + if (tryName := UIA.IUIAutomationLegacyIAccessiblePattern.Prototype.HasMethod(Name) ? Name : UIA.IUIAutomationLegacyIAccessiblePattern.Prototype.HasMethod(NewName) ? NewName : "") && this.IsLegacyIAccessiblePatternAvailable + return this.LegacyIAccessiblePattern.%tryName%(Params*) + throw MethodError("Method " Name " not found in " this.__Class " Class.",-1,Name) + } + ; Returns all direct children of the element + Children { + get { + local found + return (ComCall(6, this, "int", 2, "ptr", UIA.TrueCondition, "ptr*", &found := 0), found) ? UIA.IUIAutomationElementArray(found).ToArray() : [] + } + } + ; Returns direct children in reverse order (also the index in the enumerator counts downwards) + ReversedChildren { + get { + local found + return (ComCall(6, this, "int", 2, "ptr", UIA.TrueCondition, "ptr*", &found := 0), found) ? UIA.IUIAutomationElementArray(found).ToArray().DefineProp("__Enum", {call: (s, i) => (i = 1 ? (i := s.Length, (&v) => i > 0 ? (v := s[i], i--) : false) : (i := s.Length, (&k, &v) => (i > 0 ? (k := i, v := s[i], i--) : false)))}) : [] + } + } + ; Retrieves the cached child elements of this UI Automation element. + ; The view of the returned collection is determined by the TreeFilter property of the IUIAutomationCacheRequest that was active when this element was obtained. + ; Children are cached only if the scope of the cache request included TreeScope_Subtree, TreeScope_Children, or TreeScope_Descendants. + ; If the cache request specified that children were to be cached at this level, but there are no children, the value of this property is 0. However, if no request was made to cache children at this level, an attempt to retrieve the property returns an error. + CachedChildren { + get { + local children + return (ComCall(19, this, "ptr*", &children := 0), children) ? UIA.IUIAutomationElementArray(children).ToArray() : [] + } + } + ReversedCachedChildren { + get { + local children + return (ComCall(19, this, "ptr*", &children := 0), children) ? UIA.IUIAutomationElementArray(children).ToArray().DefineProp("__Enum", {call: (s, i) => (i = 1 ? (i := s.Length, (&v) => i > 0 ? (v := s[i], i--) : false) : (i := s.Length, (&k, &v) => (i > 0 ? (k := i, v := s[i], i--) : false)))}) : [] + } + } + + GetCachedChildren(scope:=2) { + local children + scope := UIA.TypeValidation.TreeScope(scope) + if this.HasOwnProp("CachedChildrenTreeScope" (UIA.TreeScope[scope & ~1])) { + if scope&1 { + local children := this.CachedChildrenTreeScope%UIA.TreeScope[scope]%.Clone() + children.Push(this) + return children + } + return this.CachedChildrenTreeScope%UIA.TreeScope[scope & ~1]% + } + if scope&4 { + children := [] + AppendChildren(this) + } else if scope&2 { + children := this.CachedChildren + } + if scope&1 + this.DefineProp("CachedChildrenTreeScope" UIA.TreeScope[scope], {value:children.Clone()}), children.Push(this) + else ; do not create circular reference + this.DefineProp("CachedChildrenTreeScope" UIA.TreeScope[scope], {value:children}) + return children + AppendChildren(el) { + local subchildren := el.CachedChildren, child + for child in subchildren { + children.Push(child) + AppendChildren(child) + } + } + } + ; Returns the parent of the element + Parent => UIA.TreeWalkerTrue.GetParentElement(this) + ; Retrieves from the cache the parent of this UI Automation element. + CachedParent { + get { + local parent + if (ComCall(18, this, "ptr*", &parent := 0), parent) + return UIA.IUIAutomationElement(parent) + throw UnsetError("No element returned by " A_ThisFunc, -1) + } + } + ; Returns the number of children of the element + Length => this.GetChildren().Length + /** + * Returns an object containing the location of the element + * @returns {Object} {x: screen x-coordinate, y: screen y-coordinate, w: width, h: height} + */ + Location => (br := this.BoundingRectangle, {x: br.l, y: br.t, w: br.r-br.l, h: br.b-br.t}) + /** + * Returns an object containing the last cached location of the element + * @returns {Object} {x: screen x-coordinate, y: screen y-coordinate, w: width, h: height} + */ + CachedLocation => (br := this.CachedBoundingRectangle, {x: br.l, y: br.t, w: br.r-br.l, h: br.b-br.t}) + + ; Gets or sets the current value of the element, if the ValuePattern is supported. + Value { + get { + try return this.ValuePattern.Value + try return this.RangeValuePattern.Value + try return this.LegacyIAccessiblePattern.Value + Throw Error("ValuePattern, RangeValuePattern or LegacyIAccessiblePattern not found!",,-1) + } + set { + try return this.ValuePattern.SetValue(value) + try return this.RangeValuePattern.SetValue(value) + try return this.LegacyIAccessiblePattern.SetValue(value) + Throw Error("Setting the value failed! Is ValuePattern, RangeValuePattern or LegacyIAccessiblePattern supported?",,-1) + } + } + CachedValue => this.GetCachedPropertyValue(UIA.Property.ValueValue) + ; Checks whether this object still exists + Exists { + get { + try { + local br + if (br := this.BoundingRectangle) && (!(br.b - br.t) || !(br.r - br.l)) + return 0 + if this.IsOffscreen + return 0 + } catch + return 0 + return 1 + } + } + + ; Aliases for UIA.GetPattern(UIA.Pattern.PatternName) + InvokePattern => (this.DefineProp("InvokePattern", {value:this.GetPattern(UIA.Pattern.Invoke)}), this.InvokePattern) + SelectionPattern { + get { + try return (this.DefineProp("SelectionPattern", {value:this.GetPattern(UIA.Pattern.Selection2)}), this.SelectionPattern) + return (this.DefineProp("SelectionPattern", {value:this.GetPattern(UIA.Pattern.Selection )}), this.SelectionPattern) + } + } + ValuePattern => (this.DefineProp("ValuePattern" , {value:this.GetPattern(UIA.Pattern.Value )}), this.ValuePattern) + RangeValuePattern => (this.DefineProp("RangeValuePattern" , {value:this.GetPattern(UIA.Pattern.RangeValue )}), this.RangeValuePattern) + ScrollPattern => (this.DefineProp("ScrollPattern" , {value:this.GetPattern(UIA.Pattern.Scroll )}), this.ScrollPattern) + ExpandCollapsePattern => (this.DefineProp("ExpandCollapsePattern", {value:this.GetPattern(UIA.Pattern.ExpandCollapse)}), this.ExpandCollapsePattern) + GridPattern => (this.DefineProp("GridPattern" , {value:this.GetPattern(UIA.Pattern.Grid )}), this.GridPattern) + GridItemPattern => (this.DefineProp("GridItemPattern" , {value:this.GetPattern(UIA.Pattern.GridItem )}), this.GridItemPattern) + MultipleViewPattern => (this.DefineProp("MultipleViewPattern" , {value:this.GetPattern(UIA.Pattern.MultipleView )}), this.MultipleViewPattern) + WindowPattern => (this.DefineProp("WindowPattern" , {value:this.GetPattern(UIA.Pattern.Window )}), this.WindowPattern) + SelectionItemPattern => (this.DefineProp("SelectionItemPattern" , {value:this.GetPattern(UIA.Pattern.SelectionItem )}), this.SelectionItemPattern) + DockPattern => (this.DefineProp("DockPattern" , {value:this.GetPattern(UIA.Pattern.Dock )}), this.DockPattern) + TablePattern => (this.DefineProp("TablePattern" , {value:this.GetPattern(UIA.Pattern.Table )}), this.TablePattern) + TableItemPattern => (this.DefineProp("TableItemPattern" , {value:this.GetPattern(UIA.Pattern.TableItem )}), this.TableItemPattern) + TextPattern { + get { + try return (this.DefineProp("TextPattern", {value:this.GetPattern(UIA.Pattern.Text2)}), this.TextPattern) + return (this.DefineProp("TextPattern", {value:this.GetPattern(UIA.Pattern.Text )}), this.TextPattern) + } + } + TogglePattern => (this.DefineProp("TogglePattern", {value:this.GetPattern(UIA.Pattern.Toggle)}), this.TogglePattern) + TransformPattern { + get { + try return (this.DefineProp("TransformPattern", {value:this.GetPattern(UIA.Pattern.Transform2)}), this.TransformPattern) + return (this.DefineProp("TransformPattern", {value:this.GetPattern(UIA.Pattern.Transform )}), this.TransformPattern) + } + } + ScrollItemPattern => (this.DefineProp("ScrollItemPattern" , {value:this.GetPattern(UIA.Pattern.ScrollItem )}), this.ScrollItemPattern) + LegacyIAccessiblePattern => (this.DefineProp("LegacyIAccessiblePattern" , {value:this.GetPattern(UIA.Pattern.LegacyIAccessible)}), this.LegacyIAccessiblePattern) + ItemContainerPattern => (this.DefineProp("ItemContainerPattern" , {value:this.GetPattern(UIA.Pattern.ItemContainer )}), this.ItemContainerPattern) + VirtualizedItemPattern => (this.DefineProp("VirtualizedItemPattern" , {value:this.GetPattern(UIA.Pattern.VirtualizedItem )}), this.VirtualizedItemPattern) + SynchronizedInputPattern => (this.DefineProp("SynchronizedInputPattern" , {value:this.GetPattern(UIA.Pattern.SynchronizedInput)}), this.SynchronizedInputPattern) + ObjectModelPattern => (this.DefineProp("ObjectModelPattern" , {value:this.GetPattern(UIA.Pattern.ObjectModel )}), this.ObjectModelPattern) + AnnotationPattern => (this.DefineProp("AnnotationPattern" , {value:this.GetPattern(UIA.Pattern.Annotation )}), this.AnnotationPattern) + StylesPattern => (this.DefineProp("StylesPattern" , {value:this.GetPattern(UIA.Pattern.Styles )}), this.StylesPattern) + SpreadsheetPattern => (this.DefineProp("SpreadsheetPattern" , {value:this.GetPattern(UIA.Pattern.Spreadsheet )}), this.SpreadsheetPattern) + SpreadsheetItemPattern => (this.DefineProp("SpreadsheetItemPattern" , {value:this.GetPattern(UIA.Pattern.SpreadsheetItem )}), this.SpreadsheetItemPattern) + TextChildPattern => (this.DefineProp("TextChildPattern" , {value:this.GetPattern(UIA.Pattern.TextChild )}), this.TextChildPattern) + DragPattern => (this.DefineProp("DragPattern" , {value:this.GetPattern(UIA.Pattern.Drag )}), this.DragPattern) + DropTargetPattern => (this.DefineProp("DropTargetPattern" , {value:this.GetPattern(UIA.Pattern.DropTarget )}), this.DropTargetPattern) + TextEditPattern => (this.DefineProp("TextEditPattern" , {value:this.GetPattern(UIA.Pattern.TextEdit )}), this.TextEditPattern) + CustomNavigationPattern => (this.DefineProp("CustomNavigationPattern" , {value:this.GetPattern(UIA.Pattern.CustomNavigation )}), this.CustomNavigationPattern) + ; Aliases for UIA.GetCachedPattern(UIA.Pattern.PatternName) + CachedInvokePattern => (this.DefineProp("CachedInvokePattern" , {value:this.GetCachedPattern(UIA.Pattern.Invoke)}), this.CachedInvokePattern) + CachedSelectionPattern { + get { + try return (this.DefineProp("CachedSelectionPattern", {value:this.GetCachedPattern(UIA.Pattern.Selection2)}), this.CachedSelectionPattern) + return (this.DefineProp("CachedSelectionPattern", {value:this.GetCachedPattern(UIA.Pattern.Selection )}), this.CachedSelectionPattern) + } + } + CachedValuePattern => (this.DefineProp("CachedValuePattern" , {value:this.GetCachedPattern(UIA.Pattern.Value )}), this.CachedValuePattern) + CachedRangeValuePattern => (this.DefineProp("CachedRangeValuePattern" , {value:this.GetCachedPattern(UIA.Pattern.RangeValue )}), this.CachedRangeValuePattern) + CachedScrollPattern => (this.DefineProp("CachedScrollPattern" , {value:this.GetCachedPattern(UIA.Pattern.Scroll )}), this.CachedScrollPattern) + CachedExpandCollapsePattern => (this.DefineProp("CachedExpandCollapsePattern" , {value:this.GetCachedPattern(UIA.Pattern.ExpandCollapse)}), this.CachedExpandCollapsePattern) + CachedGridPattern => (this.DefineProp("CachedGridPattern" , {value:this.GetCachedPattern(UIA.Pattern.Grid )}), this.CachedGridPattern) + CachedGridItemPattern => (this.DefineProp("CachedGridItemPattern" , {value:this.GetCachedPattern(UIA.Pattern.GridItem )}), this.CachedGridItemPattern) + CachedMultipleViewPattern => (this.DefineProp("CachedMultipleViewPattern" , {value:this.GetCachedPattern(UIA.Pattern.MultipleView )}), this.CachedMultipleViewPattern) + CachedWindowPattern => (this.DefineProp("CachedWindowPattern" , {value:this.GetCachedPattern(UIA.Pattern.Window )}), this.CachedWindowPattern) + CachedSelectionItemPattern => (this.DefineProp("CachedSelectionItemPattern" , {value:this.GetCachedPattern(UIA.Pattern.SelectionItem )}), this.CachedSelectionItemPattern) + CachedDockPattern => (this.DefineProp("CachedDockPattern" , {value:this.GetCachedPattern(UIA.Pattern.Dock )}), this.CachedDockPattern) + CachedTablePattern => (this.DefineProp("CachedTablePattern" , {value:this.GetCachedPattern(UIA.Pattern.Table )}), this.CachedTablePattern) + CachedTableItemPattern => (this.DefineProp("CachedTableItemPattern" , {value:this.GetCachedPattern(UIA.Pattern.TableItem )}), this.CachedTableItemPattern) + CachedTextPattern { + get { + try return (this.DefineProp("CachedTextPattern", {value:this.GetCachedPattern(UIA.Pattern.Text2)}), this.CachedTextPattern) + return (this.DefineProp("CachedTextPattern", {value:this.GetCachedPattern(UIA.Pattern.Text )}), this.CachedTextPattern) + } + } + CachedTogglePattern => (this.DefineProp("CachedTogglePattern" , {value:this.GetCachedPattern(UIA.Pattern.Toggle)}), this.CachedTogglePattern) + CachedTransformPattern { + get { + try return (this.DefineProp("CachedTransformPattern", {value:this.GetCachedPattern(UIA.Pattern.Transform2)}), this.CachedTransformPattern) + return (this.DefineProp("CachedTransformPattern", {value:this.GetCachedPattern(UIA.Pattern.Transform )}), this.CachedTransformPattern) + } + } + CachedScrollItemPattern => (this.DefineProp("CachedScrollItemPattern" , {value:this.GetCachedPattern(UIA.Pattern.ScrollItem )}), this.CachedScrollItemPattern) + CachedLegacyIAccessiblePattern => (this.DefineProp("CachedLegacyIAccessiblePattern" , {value:this.GetCachedPattern(UIA.Pattern.LegacyIAccessible)}), this.CachedLegacyIAccessiblePattern) + CachedItemContainerPattern => (this.DefineProp("CachedItemContainerPattern" , {value:this.GetCachedPattern(UIA.Pattern.ItemContainer )}), this.CachedItemContainerPattern) + CachedVirtualizedItemPattern => (this.DefineProp("CachedVirtualizedItemPattern" , {value:this.GetCachedPattern(UIA.Pattern.VirtualizedItem )}), this.CachedVirtualizedItemPattern) + CachedSynchronizedInputPattern => (this.DefineProp("CachedSynchronizedInputPattern" , {value:this.GetCachedPattern(UIA.Pattern.SynchronizedInput)}), this.CachedSynchronizedInputPattern) + CachedObjectModelPattern => (this.DefineProp("CachedObjectModelPattern" , {value:this.GetCachedPattern(UIA.Pattern.ObjectModel )}), this.CachedObjectModelPattern) + CachedAnnotationPattern => (this.DefineProp("CachedAnnotationPattern" , {value:this.GetCachedPattern(UIA.Pattern.Annotation )}), this.CachedAnnotationPattern) + CachedStylesPattern => (this.DefineProp("CachedStylesPattern" , {value:this.GetCachedPattern(UIA.Pattern.Styles )}), this.CachedStylesPattern) + CachedSpreadsheetPattern => (this.DefineProp("CachedSpreadsheetPattern" , {value:this.GetCachedPattern(UIA.Pattern.Spreadsheet )}), this.CachedSpreadsheetPattern) + CachedSpreadsheetItemPattern => (this.DefineProp("CachedSpreadsheetItemPattern" , {value:this.GetCachedPattern(UIA.Pattern.SpreadsheetItem )}), this.CachedSpreadsheetItemPattern) + CachedTextChildPattern => (this.DefineProp("CachedTextChildPattern" , {value:this.GetCachedPattern(UIA.Pattern.TextChild )}), this.CachedTextChildPattern) + CachedDragPattern => (this.DefineProp("CachedDragPattern" , {value:this.GetCachedPattern(UIA.Pattern.Drag )}), this.CachedDragPattern) + CachedDropTargetPattern => (this.DefineProp("CachedDropTargetPattern" , {value:this.GetCachedPattern(UIA.Pattern.DropTarget )}), this.CachedDropTargetPattern) + CachedTextEditPattern => (this.DefineProp("CachedTextEditPattern" , {value:this.GetCachedPattern(UIA.Pattern.TextEdit )}), this.CachedTextEditPattern) + CachedCustomNavigationPattern => (this.DefineProp("CachedCustomNavigationPattern" , {value:this.GetCachedPattern(UIA.Pattern.CustomNavigation )}), this.CachedCustomNavigationPattern) + + /** + * Returns the children of this element, optionally filtering by a condition + * @param c Optional UIA condition object. Default is TrueCondition. + * @param scope Optional TreeScope value: Element, Children, Family (Element+Children), Descendants, Subtree (=Element+Descendants). Default is Children. + * @returns {Array} + */ + GetChildren(c?, scope:=0x2) => this.FindAll(c ?? UIA.TrueCondition, scope) + + ; Get all child elements using TreeWalker. This is only for debugging purposes. + TWGetChildren() { + local arr := [], nextChild + if !IsObject(nextChild := UIA.TreeWalkerTrue.GetFirstChildElement(this)) + return "" + arr.Push(nextChild) + while IsObject(nextChild := UIA.TreeWalkerTrue.GetNextSiblingElement(nextChild)) + arr.Push(nextChild) + return arr + } + /** + * Returns info about the element: Type, Name, Value, LocalizedType, AutomationId, AcceleratorKey. + * @param scope Optional TreeScope value: Element, Children, Family (Element+Children), Descendants, Subtree (=Element+Descendants). Default is Descendants. + * @param delimiter Optional delimiter to separate the returned properties. Default is space. + * @param maxDepth Optional maximal depth of tree levels traversal. Default is unlimited. + * @returns {String} + */ + Dump(scope:=1, delimiter:=" ", maxDepth:=-1) { + local out := "", n, oChild, el + if !IsInteger(scope) + try scope := UIA.TreeScope.%scope% + ; Create cache request, add all the necessary properties that Dump uses: Type, LocalizedType, AutomationId, Name, Value, ClassName, AcceleratorKey + ; Don't even get the live element, because we don't need it. Gives a significant speed improvement. + static cacheRequest := UIA.CreateCacheRequest(["Type", "LocalizedType", "AutomationId", "Name", "Value", "ClassName", "AcceleratorKey"],,5,0) + ; Speed-testing has shown that DumpAll with Current properties is about 1,5 to 4x slower than with Cached properties. + ; Cached Dump with TreeScope.Element or TreeScope.Children is a bit slower than Current Dump. + ; This means that we use Current properties if TreeScope is less than Descendants, + ; and Cached properties if dumping Descendants. This gives the maximum performance. + if scope < 4 { + if scope&1 { + out := this.CurrentDump(delimiter) "`n" + } + if scope&2 { + for n, oChild in this.GetChildren() + out .= n ": " oChild.CurrentDump(delimiter) "`n" + } + } else { + ; Build the cache for the set scope + try el := this.CachedChildren.Length + el := IsSet(el) && el ? this : this.FindFirstBuildCache(cacheRequest, UIA.RawViewCondition, scope) + if scope&1 + out := el.CachedDump(delimiter) "`n" + if scope&2 { + for n, oChild in el.CachedChildren + out .= n ": " oChild.CachedDump(delimiter) "`n" + } + if scope&4 + return RTrim(RecurseTree(el, out), "`n") + } + return RTrim(out, "`n") + + RecurseTree(element, tree, path:="") { + local i, child, count + if maxDepth > 0 { + StrReplace(path, "," , , , &count) + if count >= (maxDepth-1) + return tree + } + For i, child in element.CachedChildren { + tree .= path (path?",":"") i ":" delimiter child.CachedDump(delimiter) "`n" + tree := RecurseTree(child, tree, path (path?",":"") i) + } + return tree + } + } + ; Dumps with only "Current" properties + CurrentDump(delimiter:=" ") { + local ctrlType, name, val, lct, aid, cm, ak + return "Type: " (ctrlType := this.Type) " (" UIA.Type[ctrlType] ")" ((name := this.Name) == "" ? "" : delimiter "Name: `"" name "`"") ((val := this.Value) == "" ? "" : delimiter "Value: `"" val "`"") ((lct := this.LocalizedType) == "" ? "" : delimiter "LocalizedType: `"" lct "`"") ((aid := this.AutomationId) == "" ? "" : delimiter "AutomationId: `"" aid "`"") ((cm := this.ClassName) == "" ? "" : delimiter "ClassName: `"" cm "`"") ((ak := this.AcceleratorKey) == "" ? "" : delimiter "AcceleratorKey: `"" ak "`"") + } + ; Dumps with only "Cached" properties + CachedDump(delimiter:=" ") { + local ctrlType, name, val, lct, aid, cm, ak + return "Type: " (ctrlType := this.CachedType) " (" UIA.Type[ctrlType] ")" ((name := this.CachedName) == "" ? "" : delimiter "Name: `"" name "`"") ((val := this.CachedValue) == "" ? "" : delimiter "Value: `"" val "`"") ((lct := this.CachedLocalizedType) == "" ? "" : delimiter "LocalizedType: `"" lct "`"") ((aid := this.CachedAutomationId) == "" ? "" : delimiter "AutomationId: `"" aid "`"") ((cm := this.CachedClassName) == "" ? "" : delimiter "ClassName: `"" cm "`"") ((ak := this.CachedAcceleratorKey) == "" ? "" : delimiter "AcceleratorKey: `"" ak "`"") + } + ToString() { + try return this.Dump() + return this.CachedDump() + } + /** + * Returns info about the element and its descendants: Type, Name, Value, LocalizedType, AutomationId, AcceleratorKey. + * @param maxDepth Optional maximal depth of tree levels traversal. Default is unlimited. + * @returns {String} + */ + DumpAll(delimiter:=" ", maxDepth:=-1) => this.Dump(5, delimiter, maxDepth) + + /** + * Returns an UIA path leading to the target element. + * This path consists of Type and index information. + * @param targetEl The element the path will lead to from this element + * @param cached Whether this element contains a cached tree that should be used for the search. + * Since UIA path requires Type information, the Type should be cached in the tree. + * @returns {String} + */ + GetUIAPath(targetEl, cached := False) => UIA.EncodePath(this.GetTypeConditionPath(targetEl, cached)) + /** + * Returns an array of conditions consisting of Type and index information, defining the path leading to the target element. + * @param targetEl The element the path will lead to from this element + * @param cached Whether this element contains a cached tree that should be used for the search. + * The cached tree must contain Type, Name, ClassName, AutomationId, and BoundingRectangle. + * @returns {Array} + */ + GetTypeConditionPath(targetEl, cached := False) { + local i, child, cachedThis, numPath, conditionPath, children, targetType, targetIndex, sameTypeCount, targetTypeIndex + cachedThis := cached ? this : this.BuildUpdatedCache(UIA.CreateCacheRequest(["Type", "Name", "ClassName", "AutomationId", "BoundingRectangle"],,5)) + numPath := cachedThis.GetNumericPath(targetEl, true) + conditionPath := [] + Loop numPath.Length { + children := cachedThis.CachedChildren, targetIndex := numPath[A_index], sameTypeCount := 0, targetTypeIndex := 0 + targetType := children[targetIndex].CachedType + for i, child in children { + if child.CachedType == targetType + sameTypeCount++ + if i == targetIndex + targetTypeIndex := sameTypeCount + } + if targetTypeIndex = 1 + conditionPath.Push({Type:targetType}) + else if targetTypeIndex = sameTypeCount + conditionPath.Push({Type:targetType, i:-1}) + else + conditionPath.Push({Type:targetType, i:targetTypeIndex}) + cachedThis := children[targetIndex] + } + return conditionPath + } + + /** + * Returns an array of conditions consisting of a (hopefully) unique combination of Type, ClassName, + * AutomationId, Name, and index information, defining the path leading to the target element. + * @param targetEl The element the path will lead to from this element + * @param cached Whether this element contains a cached tree that should be used for the search. + * The cached tree must contain Type, Name, ClassName, AutomationId, and BoundingRectangle. + * @param ignoreNames If set to True then the Name property will not be used. This might make + * the path more reliable in certain circumstances where the Name property can change. + * @param asString Returns the path as a string rather than an array of objects. + * @returns {Array} + */ + GetConditionPath(targetEl, cached := False, ignoreNames := False, asString := False) { + local i, child, cachedThis, numPath, conditionPath, children, targetIndex, sameTypeCount, targetTypeIndex + cachedThis := cached ? this : this.BuildUpdatedCache(UIA.CreateCacheRequest(["Type", "Name", "ClassName", "AutomationId", "BoundingRectangle"],,5)) + numPath := cachedThis.GetNumericPath(targetEl, true) + conditionPath := asString ? "" : [] + Loop numPath.Length { + children := cachedThis.CachedChildren, targetIndex := numPath[A_index], sameTypeCount := 0, targetTypeIndex := 0 + ; Create compact condition map for the level + local levelMap := Map(), levelConditions := [], t := "", a := "", n := "", c := "", type, automationId, className, name + + ; First map all the children with the appropriate conditions to figure out whether to use index 1 or -1 + for i, child in children { + type := child.CachedType, t := "{T:" (type-50000) + ; levelMap will contain the max index of an element with certain Type, Type+AutomationId, Type+Name, and Type+ClassName + levelMap[t] := levelMap.Has(t) ? levelMap[t] + 1 : 1 + try a := StrReplace(automationId := child.CachedAutomationId, "`"", "```"") + ; Currently AutomationId will be paired with the Type, but perhaps this isn't necessary because AutomationId should be unique? + if a != "" && !IsInteger(a) ; Ignore Integer AutomationIds, since they seem to be auto-generated in Chromium apps + a := t ",A:`"" a "`"", levelMap[a] := levelMap.Has(a) ? levelMap[a] + 1 : 1 ; This actually shouldn't be needed, if AutomationId's are unique + try c := StrReplace(className := child.CachedClassName, "`"", "```"") + if c != "" + c := t ",CN:`"" c "`"", levelMap[c] := levelMap.Has(c) ? levelMap[c] + 1 : 1 + ; Consider Name last, because it can change (eg. window title) + try n := StrReplace(name := child.CachedName, "`"", "```"") + if !IgnoreNames && n != "" + n := t ",N:`"" n "`"", levelMap[n] := levelMap.Has(n) ? levelMap[n] + 1 : 1 + if levelMap[t] = 1 ; If this is the first element with such a type then only use the type (not including AutomationId etc, to keep the resulting object shorter) + levelConditions.Push([{T:type-50000, i:levelMap[t]}, t]) + else if a != "" && !IsInteger(a) { + levelConditions.Push(c != "" ? (levelMap[a] <= levelMap[c] ? [{A:automationId, i:levelMap[a]}, a] : [{CN: className, i:levelMap[c]}, c]) : [{A:automationId, i:levelMap[a]}, a]) + } else if c != "" + levelConditions.Push([{CN: className, i:levelMap[c]}, c]) + else if !IgnoreNames && n != "" && (levelMap[n] < levelMap[t]) + levelConditions.Push([{N: name, i:levelMap[n]}, n]) + else + levelConditions.Push([{T:type-50000, i:levelMap[t]}, t]) + } + + levelCondition := levelConditions[targetIndex] + if asString + conditionPath .= ((levelCondition[1].i = 1) ? levelCondition[2] : (levelCondition[1].i = levelMap[levelCondition[2]] ? (levelCondition[1].i := -1, levelCondition[2] ", i:-1") : (levelCondition[2] ", i:" levelCondition[1].i))) "}" (A_Index = numPath.Length ? "" : ", ") + else + conditionPath.Push((levelCondition[1].i = 1) ? (levelCondition[1].DeleteProp("i"), levelCondition[1]) : (levelCondition[1].i = levelMap[levelCondition[2]] ? (levelCondition[1].i := -1, levelCondition[1]) : levelCondition[1])) + cachedThis := children[targetIndex] + } + return conditionPath + } + + /** + * Returns an array of integers defining the numeric path leading to the target element. + * @param targetEl The element the path will lead to from this element + * @param cached Whether this element contains a cached tree that should be used for the search. + * The cached tree must contain Type, Name, ClassName, AutomationId, and BoundingRectangle. + * @returns {Array} + */ + GetNumericPath(targetEl, cached := False) { + local cachedThis, found, cacheRequest + cachedThis := cached ? this : this.BuildUpdatedCache(cacheRequest := UIA.CreateCacheRequest(["Type", "Name", "ClassName", "AutomationId", "BoundingRectangle"],,5)) + if found := FindTarget(cachedThis) + return found + throw TargetError("No matching target element found", -1) + + FindTarget(el, path:=[]) { + local i, child + for i, child in el.CachedChildren { + path.Push(i) + if UIA.CompareElementsEx(child, targetEl) + return path + if found := FindTarget(child, path) + return found + path.Pop() + } + } + } + + /** + * @param relativeTo CoordMode to be used: client, window or screen. Default is A_CoordModeMouse + * @param WinTitle Optional: may be used to specifically define the window that the element belongs to + * @returns {x:x coordinate, y:y coordinate, w:width, h:height} + */ + GetPos(relativeTo:="", WinTitle?) { + if IsSet(WinTitle) && !IsInteger(WinTitle) + WinTitle := WinExist(WinTitle) + local br := this.BoundingRectangle, pt + relativeTo := (relativeTo == "") ? A_CoordModeMouse : relativeTo + if (relativeTo = "screen") + return {x:br.l, y:br.t, w:(br.r-br.l), h:(br.b-br.t)} + else if (relativeTo = "window") { + DllCall("user32\GetWindowRect", "Int", WinTitle ?? this.WinId, "Ptr", RECT := Buffer(16)) + return {x:(br.l-NumGet(RECT, 0, "Int")), y:(br.t-NumGet(RECT, 4, "Int")), w:br.r-br.l, h:br.b-br.t} + } else if (relativeTo = "client") { + pt := Buffer(8), NumPut("int",br.l,pt), NumPut("int", br.t,pt,4) + DllCall("ScreenToClient", "Int", WinTitle ?? this.WinId, "Ptr", pt) + return {x:NumGet(pt,0,"int"), y:NumGet(pt,4,"int"), w:br.r-br.l, h:br.b-br.t} + } else + throw Error(relativeTo "is not a valid CoordMode",-1) + } + + ; Gets the Id in string format (currently the RuntimeId). The exact implementation is subject to + ; change in the future, so use of RuntimeId might be preferred. It can't be used for element searches! + Id => (this.DefineProp("Id", {value:this.RuntimeId}), this.Id) + ; Gets the RuntimeId in string format + RuntimeId => (this.DefineProp("RuntimeId", {value:UIA.RuntimeIdToString(this.GetRuntimeId())}), this.RuntimeId) + + ; Get the parent window hwnd from the element + GetWinId() { + static cacheRequest := UIA.CreateCacheRequest(["NativeWindowHandle"],,1, UIA.AutomationElementMode.None), TW := UIA.CreateTreeWalker(UIA.CreateNotCondition(UIA.CreatePropertyCondition(UIA.Property.NativeWindowHandle, 0))) + try return DllCall("GetAncestor", "Ptr", TW.NormalizeElementBuildCache(cacheRequest, this).CachedNativeWindowHandle, "UInt", 2, "Ptr") ; hwnd from point by SKAN + } + WinId => (this.DefineProp("WinId", {value:this.GetWinId()}), this.WinId) + ; Get the control hwnd (that the element belongs to) from the element + GetControlId() { + static cacheRequest := UIA.CreateCacheRequest(["NativeWindowHandle"],,1, UIA.AutomationElementMode.None), TW := UIA.CreateTreeWalker(UIA.CreateNotCondition(UIA.CreatePropertyCondition(UIA.Property.NativeWindowHandle, 0))) + try return TW.NormalizeElementBuildCache(cacheRequest, this).CachedNativeWindowHandle + } + ControlId => (this.DefineProp("ControlId", {value:this.GetControlId()}), this.ControlId) + + /** + * Tries to click the element. The method depends on WhichButton variable: by default it is attempted + * to use any "click"-like methods, such as InvokePattern Invoke(), TogglePattern Toggle(), SelectionItemPattern Select(). + * @param WhichButton + * * If WhichButton is left empty (default), then any "click"-like pattern methods + * will be used (Invoke(), Toggle(), Select() etc. + * * If WhichButton is a number, then Sleep will be called afterwards with that number of milliseconds. + * Eg. Element.Click(200) will sleep 200ms after "clicking". + * * If WhichButton is "left" or "right", then the AHK Click() will be used to move the cursor to + * the center of the element and perform a click. + * @param ClickCount Is used if WhichButton isn't a number or left empty, that is if AHK Click() + * will be used. In this case if ClickCount is a number <10, then that number of clicks will be performed. + * If ClickCount is >=10, then Sleep will be called with that number of ms. Both ClickCount and sleep time + * can be combined by separating with a space. + * Eg. Element.Click("left", 1000) will sleep 1000ms after clicking. + * Element.Click("left", 2) will double-click the element + * Element.Click("left", "2 1000") will double-click the element and then sleep for 1000ms + * @param DownOrUp If AHK Click is used, then this is the same as AHK Click DownOrUp parameter. + * @param Relative If Relative is "Rel" or "Relative" then X and Y coordinates are treated as offsets from the current mouse position. + * Otherwise it expects offset values for both X and Y (eg "-5 10" would offset X by -5 and Y by +10 from the center of the element). + * @param NoActivate If AHK Click is used, then this will determine whether the window is activated + * before clicking if the clickable point isn't visible on the screen. Default is activating. + * @param MoveBack If set then the cursor will be moved back to its original location after sleeping for + * the specified amount of ms. Specify 0 for no sleep. + */ + Click(WhichButton:="", ClickCount:=1, DownOrUp:="", Relative:="", NoActivate:=False, MoveBack?) { + local SleepTime, togglePattern, expandState, selectionPattern, rel, pos + if WhichButton = "" or IsInteger(WhichButton) { + ; Caching the properties doesn't seem to make a speed difference, rather it might be slower with caching + ; Using DoDefaultAction as default might end up slower (tested in Chrome - clicked much slower) + if (this.GetPropertyValue(UIA.Property.IsInvokePatternAvailable)) { + this.InvokePattern.Invoke() + if WhichButton != "" + Sleep WhichButton + return "Invoke" + } + if (this.GetPropertyValue(UIA.Property.IsTogglePatternAvailable)) { + togglePattern := this.TogglePattern, toggleState := togglePattern.CurrentToggleState + togglePattern.Toggle() + if (togglePattern.CurrentToggleState != toggleState) { + if WhichButton != "" + Sleep WhichButton + return "Toggle" + } + } + if (this.GetPropertyValue(UIA.Property.IsExpandCollapsePatternAvailable)) { + if ((expandState := (pattern := this.ExpandCollapsePattern).ExpandCollapseState) == 0) { + try pattern.Expand() ; Sometimes throws UIA_E_INVALIDOPERATION 0x80131509 + action := "Expand" + } else { + try pattern.Collapse() + action := "Collapse" + } + if (pattern.ExpandCollapseState != expandState) { + if WhichButton != "" + Sleep WhichButton + return action + } + } + if (this.GetPropertyValue(UIA.Property.IsSelectionItemPatternAvailable)) { + selectionPattern := this.SelectionItemPattern, selectionState := selectionPattern.IsSelected + selectionPattern.Select() + if (selectionPattern.IsSelected != selectionState) { + if WhichButton != "" + Sleep WhichButton + return "Select" + } + } + if (this.GetPropertyValue(UIA.Property.IsLegacyIAccessiblePatternAvailable)) { + this.LegacyIAccessiblePattern.DoDefaultAction() + if WhichButton != "" + Sleep WhichButton + return "DoDefaultAction" + } + return 0 + } + rel := [0,0], pos := this.Location + if (Relative && !InStr(Relative, "rel")) + rel := StrSplit(Relative, " "), Relative := "" + if IsInteger(WhichButton) + SleepTime := WhichButton, WhichButton := "left" + if !IsInteger(ClickCount) && InStr(ClickCount, " ") { + sCount := StrSplit(ClickCount, " ") + ClickCount := sCount[1], SleepTime := sCount[2] + } else if ClickCount > 9 { + SleepTime := ClickCount, ClickCount := 1 + } + if (!NoActivate && (UIA.WindowFromPoint(pos.x+pos.w//2+rel[1], pos.y+pos.h//2+rel[2]) != (wId := this.WinId))) { + WinActivate(wId) + WinWaitActive(wId) + } + saveCoordMode := A_CoordModeMouse + CoordMode("Mouse", "Screen") + if IsSet(MoveBack) + MouseGetPos(&prevX, &prevY) + Click(pos.x+pos.w//2+rel[1] " " pos.y+pos.h//2+rel[2] " " WhichButton (ClickCount ? " " ClickCount : "") (DownOrUp ? " " DownOrUp : "") (Relative ? " " Relative : "")) + if IsSet(MoveBack) { + if MoveBack != 0 + Sleep MoveBack + MouseMove(prevX, prevY) + } + CoordMode("Mouse", saveCoordMode) + if IsSet(SleepTime) + Sleep(SleepTime) + } + + /** + * Uses ControlClick to click the element. + * @param WhichButton determines which button to use to click (left, right, middle). + * If WhichButton is a number, then a Sleep will be called afterwards. + * Eg. ControlClick(200) will sleep 200ms after clicking. + * @param ClickCount How many times to click. Default is 1. + * @param Options Additional ControlClick Options (see AHK documentations). + * @param WinTitle Optional: providing the WinTitle of the window the element belongs to might + * give speed improvements and sometimes be more reliable. + */ + ControlClick(WhichButton:="left", ClickCount:=1, Options:="", WinTitle?) { + local pos := this.GetPos("client", WinTitle?) + ControlClick("X" pos.x+pos.w//2 " Y" pos.y+pos.h//2, WinTitle ?? this.WinId,, IsInteger(WhichButton) ? "left" : WhichButton, ClickCount, Options) + if IsInteger(WhichButton) + Sleep(WhichButton) + } + /** + * Highlights the element for a chosen period of time. + * @param showTime Can be one of the following: + * * Unset - if highlighting exists then removes the highlighting, otherwise highlights for 2 seconds. This is the default value. + * * 0 - Indefinite highlighting + * * Positive integer (eg 2000) - will highlight and pause for the specified amount of time in ms + * * Negative integer - will highlight for the specified amount of time in ms, but script execution will continue + * * "clear" - removes the highlighting unconditionally + * @param color The color of the highlighting. Default is red. + * @param d The border thickness of the highlighting in pixels. Default is 2. + * @returns {UIA.IUIAutomationElement} + */ + Highlight(showTime:=unset, color:="Red", d:=2) { + local loc, x, y, w, h, key, prop + static Guis := Map() + if IsSet(showTime) && showTime = "clearall" { + for key, prop in Guis { + SetTimer(prop.TimerObj, 0) + try prop.GuiObj.Destroy() + } + Guis := Map() + return this + } + + if (!IsSet(showTime) && Guis.Has(this.ptr)) || (IsSet(showTime) && showTime = "clear") { + if Guis.Has(this.ptr) { + SetTimer(Guis[this.ptr].TimerObj, 0) + try Guis[this.ptr].GuiObj.Destroy() + Guis.Delete(this.ptr) + } + return this + } else if !IsSet(showTime) + showTime := 2000 + + try loc := this.BoundingRectangle + if !IsSet(loc) || !IsObject(loc) + try loc := this.CachedBoundingRectangle + if !IsSet(loc) || !IsObject(loc) + return this + + if !Guis.Has(this.ptr) { + Guis[this.ptr] := {} + Guis[this.ptr].GuiObj := Gui("+AlwaysOnTop -Caption +ToolWindow -DPIScale +E0x08000000") + Guis[this.ptr].TimerObj := ObjBindMethod(this, "Highlight", "clear") + } + GuiObj := Guis[this.ptr].GuiObj + GuiObj.BackColor := color + x:=loc.l, y:=loc.t, w:=(loc.r-loc.l), h:=(loc.b-loc.t), iw:= w+d, ih:= h+d, w:=w+d*2, h:=h+d*2, x:=x-d, y:=y-d + WinSetRegion("0-0 " w "-0 " w "-" h " 0-" h " 0-0 " d "-" d " " iw "-" d " " iw "-" ih " " d "-" ih " " d "-" d, GuiObj.Hwnd) + GuiObj.Show("NA x" . x . " y" . y . " w" . w . " h" . h) + + if showTime > 0 { + Sleep(showTime) + this.Highlight() + } else if showTime < 0 + SetTimer(Guis[this.ptr].TimerObj, -Abs(showTime)) + return this + } + ClearHighlight() => this.Highlight("clear") + + /** + * Waits for this element to not exist + * @param timeout Optional: Waiting time for element to disappear. Default: indefinite wait + * @param tick Optional: Wait time between each check. Default: 20ms + * @returns {Integer} + */ + WaitNotExist(timeOut := -1, tick := 20) { + local endtime := A_TickCount + timeout, exists + while ((exists := this.Exists) && ((timeout == -1) || (A_Tickcount < endtime))) + Sleep tick + return !exists + } + + static __ExtractConditionNamedParameters(condition, &scope, &order, &startingElement, &cacheRequest?, &index?, &callback:=0) { + if Type(condition) != "Object" { + if IsObject(condition) && HasMethod(condition) { + callback := condition + return {} + } + return condition + } + pureCondition := condition.Clone() + if IsSet(index) { + index := pureCondition.DeleteProp("index") || pureCondition.DeleteProp("i") || index + if IsInteger(index) && !(index is Integer) + index := Integer(index) + } + scope := pureCondition.DeleteProp("scope") || scope + order := pureCondition.DeleteProp("order") || order + startingElement := pureCondition.DeleteProp("startingElement") || startingElement + if IsSet(cacheRequest) + cacheRequest := pureCondition.DeleteProp("cacheRequest") || cacheRequest + callback := pureCondition.DeleteProp("callback") || callback + return pureCondition + } + + /** + * Checks whether an element that matches the specified condition exists within the provided TreeScope. + * If no element is found, then 0 is returned. + * @param condition The condition to filter with, or a callback function. + * The condition object additionally supports named parameters. + * See a more detailed explanation under FindElement condition argument. + * Default MatchMode is "Exact", and CaseSense "On". + * Note: MatchMode "StartsWith" and "RegEx" will have the performance of FindElements (slower). + * @param scope Optional TreeScope value: Element, Children, Family (Element+Children), Descendants, Subtree (=Element+Descendants). Default is Descendants. + * @param index Looks for the n-th element matching the condition + * @param order Optional: custom tree navigation order, one of UIA.TreeTraversalOptions values (LastToFirstOrder, PostOrder, LastToFirstPostOrder). Default is FirstToLast and PreOrder. [requires Windows 10 version 1703+] + * @param startingElement Optional: search will start from this element instead, which must be a child/descendant of the starting element [requires Windows 10 version 1703+] + * If startingElement is supplied then part of the tree will not be searched (depending on TreeTraversalOrder, either everything before this element, or everything after it will be ignored) + * @param cacheRequest Optional: cache request object + * @returns {UIA.IUIAutomationElement} + */ + ElementExist(condition, scope:=4, index:=1, order:=0, startingElement:=0, cacheRequest:=0) { + try return this.FindElement(condition, scope, index, order, startingElement, cacheRequest) + catch TargetError + return 0 + return 0 + } + + /** + * Retrieves the first child or descendant element that matches the specified condition. + * If no element is found, then TargetError is thrown. + * @param condition The condition to filter with, or a callback function + * + * * A single property condition consists of an object where the key is the property name, and value is the property value: `{Name:"Test"}` => Name property must match "Test" exactly + * * Everything inside {} is an "and" condition: `{Type:"Button", Name:"Something"}` => Name must match "Something" AND Type must be Button + * * Everything inside [] is an "or" condition: `[{Name:"Test"}, {Name:"Something"}]` => Name must match "Test" OR Name must match "Something" + * * Object key "not" creates a not condition: `{not:{Type:"Button"}}` => everything except Type Button + * + * * MatchMode key (short form: mm) can be one of UIA.MatchMode values: "StartsWith", "SubString", "Exact", "RegEx". Default MatchMode is "Exact". + * MatchMode applies to all string-type conditions (except Type), multiple different MatchModes are allowed if used in separate sub-conditions. + * Note: MatchMode "StartsWith" and "RegEx" will have the performance of FindElements (slower). + * * CaseSense key (short form: cs) defines case-sense: "On"/True or "Off"/False. Default is "On". + * + * * The condition object additionally supports named parameters: {Type:"Button", index:-1} => last element with Type Button + * + * * If a callback function is provided instead, then it must accept three parameters: `callback(element, index, depth)` + * where `element` is the currently evaluated element, `index` is the step index in the current layer (with + * LastToFirst this still starts from 1), and `depth` is the current depth (0 = starting element). + * If the callback returns 1 then the element is matched, if 0 then is not matched and search continues, + * and if -1 then for PreOrder search the branch under the element is skipped and for PostOrder search + * the search jumps to the parent of the element skipping the siblings. + * In the case of a callback function the tree is walked using a TreeWalker, which is created with a + * UIA.TrueCondition if no other conditions are set. The treewalker can be created with a custom condition if + * the callback is instead provided as a named argument "callback". + * Eg. `Element.FindElement({callback:MyCallback, Type:"Button"})` walks a tree consisting of button-type elements. + * Example: `Element.FindElement((el, i, depth) => depth = 1 && el.Type != UIA.Type.Document ? -1 : el.Name = "Test")` + * This callback function searches only the branch with where at depth 1 the type is Document, and + * then searches for an element named "Test". + * + * @param scope Optional TreeScope value: Element, Children, Family (Element+Children), Descendants, Subtree (=Element+Descendants). Default is Descendants. + * @param index Looks for the n-th element matching the condition + * @param order Optional: custom tree navigation order, one of UIA.TreeTraversalOptions values (LastToFirstOrder, PostOrder, LastToFirstPostOrder). Default is FirstToLast and PreOrder. [requires Windows 10 version 1703+] + * @param startingElement Optional: search will start from this element instead, which must be a child/descendant of the starting element [requires Windows 10 version 1703+] + * If startingElement is supplied then part of the tree will not be searched (depending on TreeTraversalOrder, either everything before this element, or everything after it will be ignored) + * @param cacheRequest Optional: cache request object + * @returns {UIA.IUIAutomationElement} + */ + FindElement(condition, scope:=4, index:=1, order:=0, startingElement:=0, cacheRequest:=0) { + local withOptions := 0, found, cache, callback := 0 + condition := UIA.IUIAutomationElement.__ExtractConditionNamedParameters(condition, &scope, &order, &startingElement, &cacheRequest, &index, &callback) + scope := UIA.TypeValidation.TreeScope(scope), index := UIA.TypeValidation.Integer(index, "Index"), order := UIA.TypeValidation.TreeTraversalOptions(order), startingElement := UIA.TypeValidation.Element(startingElement), cacheRequest := UIA.TypeValidation.CacheRequest(cacheRequest) + if index < 0 ; Convert negative index to LastToFirst order + index := -index, order := order | 2 + withOptions := order || startingElement + if withOptions && !UIA.IsIUIAutomationElement7Available && (Type(condition) ~= "i)Condition|ComValue") + throw ValueError("If using order or startingElement arguments in Windows <10.0.15063, then condition cannot be IUIAutomationCondition", -1) + if !(Type(condition) ~= "i)Condition|ComValue") { + /* + If MatchMode is 1: + Use MatchMode 2, but then return all the results and filter using the condition + If MatchMode is RegEx: + Find all !"" elements, then filter using the condition + If either of these is encountered, nonUIAEncountered is set to 1. + If a CachedProperty is encountered, nonUIAEncountered is set to 2 and a cached search is used instead. + */ + IUIAcondition := UIA.__ConditionBuilder(condition, &nonUIAEncountered:=0), counter := 0 + + ; In the case of a defined callback, walk the tree with a TreeWalker made on the base of + ; IUIAcondition and validate with callback(element). + if callback { + local found, TW := UIA.CreateTreeWalker(IUIAcondition), c := 0 + if startingElement + startingElement := startingElement.RuntimeId + ; FirstToLast or LastToFirst + if order&2 + nextElementFunc := UIA.IUIAutomationTreeWalker.Prototype.TryGetPreviousSiblingElement.Bind(TW), + nextChildFunc := UIA.IUIAutomationTreeWalker.Prototype.TryGetLastChildElement.Bind(TW) + else + nextElementFunc := UIA.IUIAutomationTreeWalker.Prototype.TryGetNextSiblingElement.Bind(TW), + nextChildFunc := UIA.IUIAutomationTreeWalker.Prototype.TryGetFirstChildElement.Bind(TW) + ; First handle PostOrder + if order&1 { + if IsObject(found := PostOrderRecursiveFind(this, scope)) + return cacheRequest ? found.BuildUpdatedCache(cacheRequest) : found + throw TargetError("An element matching the condition was not found", -1) + } + ; PreOrder + if scope&1 && (c := callback(this, 1, 0)) > 0 && --index = 0 + return cacheRequest ? this.BuildUpdatedCache(cacheRequest) : this + if c >= 0 && scope > 1 && found := PreOrderRecursiveFind(nextChildFunc(this)) + return cacheRequest ? found.BuildUpdatedCache(cacheRequest) : found + throw TargetError("An element matching the condition was not found", -1) + } + + if nonUIAEncountered = 1 { ; Some conditions need validating: MatchMode 1 or RegEx was used + ; If no cacheRequest was specified, then speed-optimize the search by caching + ; the required properties, and searching through cached elements. + ; And although it looks like if adding an exception for a set startingElement + ; and using FindAllWithOptions from startingElement should give a speed improvement, + ; it actually doesn't affect the search speed significantly, probably because + ; the cache search is just so much faster. + ; It would be useful to use TreeFilter=IUIAcondition, but unfortunately it seems + ; that TreeFilter is applied before TreeScope. This leads to the unwanted behavior + ; of the tree being flattened, so this can't be used with scope=Children + cache := this.BuildUpdatedCache(UIA.CreateCacheRequest(condition,,scope,, scope>3 ? IUIAcondition : unset)) + found := cache.FindCachedElement(condition, scope, index, order, startingElement) + return cacheRequest ? found.BuildUpdatedCache(cacheRequest) : found + } else if nonUIAEncountered = 2 ; A cached name was encountered, use FindCachedElement instead + return this.FindCachedElement(condition, scope, index, order, startingElement) + + ; If the user is running an older Windows, but is trying to use FindFirstWithOptions arguments + ; then either use cached elements, or in the case of index 1 and PreOrder, use TreeWalker + if withOptions && (!UIA.IsIUIAutomationElement7Available || (scope < 4 && startingElement)) { + if index = 1 && order = 2 && !startingElement { ; Special case for index -1, PreOrder, and older Windows version + if scope & 1 + try return cacheRequest ? this.FindFirstBuildCache(cacheRequest, IUIAcondition, 1) : this.FindFirst(IUIAcondition, 1) + if scope > 1 + return cacheRequest ? UIA.CreateTreeWalker(IUIAcondition).GetLastChildElementBuildCache(cacheRequest, this) : UIA.CreateTreeWalker(IUIAcondition).GetLastChildElement(this) + else + throw TargetError("An element matching the condition was not found", -1) + } + found := this.BuildUpdatedCache(UIA.CreateCacheRequest(condition,,scope,,scope>3 ? IUIAcondition : unset)).FindCachedElement(condition, scope, index, order, startingElement) + return cacheRequest ? found.BuildUpdatedCache(cacheRequest) : found + } + condition := IUIAcondition + } + if !startingElement ; If FindFirstWithOptions is used but startingElement isn't set, start from this element. + startingElement := this + ; Use FindAll instead if index > 1 + if index != 1 { + try { + unfilteredEls := (cacheRequest ? (withOptions ? startingElement.FindAllWithOptionsBuildCache(cacheRequest, condition, order, this, scope) + : this.FindAllBuildCache(cacheRequest, condition, scope)) + : (withOptions ? startingElement.FindAllWithOptions(condition, order, this, scope) + : this.FindAll(condition, scope))) + return unfilteredEls[order&2 && !withOptions ? unfilteredEls.Length + 1 - index : index] + } catch IndexError + throw TargetError("An element matching the condition was not found", -1, "FindAll index " index " was invalid") + } + if cacheRequest { + if withOptions + ComCall(112, startingElement, "int", scope, "ptr", condition, "ptr", cacheRequest, "int", order, "ptr", this, "ptr*", &found := 0) ; FindFirstWithOptionsBuildCache + else + ComCall(7, this, "int", scope, "ptr", condition, "ptr", cacheRequest, "ptr*", &found := 0) ; FindFirstBuildCache + } else { + if withOptions { + ComCall(110, startingElement, "int", scope, "ptr", condition, "int", order, "ptr", this, "ptr*", &found := 0) ; FindFirstWithOptions + } else { + ComCall(5, this, "int", scope, "ptr", condition, "ptr*", &found := 0) ; FindFirst + } + } + if found + return UIA.IUIAutomationElement(found) + throw TargetError("An element matching the condition was not found", -1) + + PreOrderRecursiveFind(el, depth:=0) { + local found + depth++ + while (c := 0, IsObject(el)) { + if (startingElement ? (startingElement = el.RuntimeId ? !(startingElement := "") && (scope & 1) : 0) : 1) && (c := callback(el, A_Index, depth)) > 0 && --index = 0 + return el + else if c >= 0 && scope&4 && IsObject(found := PreOrderRecursiveFind(nextChildFunc(el), depth)) + return found + el := nextElementFunc(el) + } + } + PostOrderRecursiveFind(el, scope, i:=1, depth:=0) { ; this is only called if scope >= 4, and handles scope&2 as well + local found, child, c := 0 + child := nextChildFunc(el), depth++ + if scope > 1 { + while (IsObject(child)) { + if IsObject(found := PostOrderRecursiveFind(child, (scope & ~2)|1, A_Index, depth)) + return found + else if found = -1 + break + child := nextElementFunc(child) + } + } + if scope&1 && (startingElement ? (startingElement = el.RuntimeId ? !(startingElement := "") && (scope & 1) : 0) : 1) && (c := callback(el, i, depth)) > 0 && --index = 0 + return el + return c + } + } + + /** + * CachedElementExist can be used to find an element inside a cached tree, using only cached properties. + * This is not a UIA native method: make sure the cached tree is reasonably small, otherwise the performance will suffer. + * If no element is found, then 0 is returned. + * @param condition The condition to filter with. + * The condition object additionally supports named parameters. + * Default MatchMode is "Exact", and CaseSense "On". + * See a more detailed explanation under FindElement condition argument. + * @param scope Optional TreeScope value: Element, Children, Family (Element+Children), Descendants, Subtree (=Element+Descendants). Default is Descendants. + * @param index Looks for the n-th element matching the condition + * @param order Optional: custom tree navigation order, one of UIA.TreeTraversalOptions values (LastToFirstOrder, PostOrder, LastToFirstPostOrder). Default is FirstToLast and PreOrder. + * @param startingElement Optional: search will start from this element instead, which must be a child/descendant of the starting element + * If startingElement is supplied then part of the tree will not be searched (depending on TreeTraversalOrder, either everything before this element, or everything after it will be ignored) + * Unlike FindElement, using this will most likely not have a beneficial effect on performance. + * @returns {UIA.IUIAutomationElement} + */ + CachedElementExist(condition, scope:=4, index:=1, order:=0, startingElement:=0) { + try return this.FindCachedElement(condition, scope, index, order, startingElement) + catch TargetError + return 0 + return 0 + } + + /** + * FindCachedElement can be used to find an element inside a cached tree, using only cached properties. + * This is not a UIA native method: make sure the cached tree is reasonably small, otherwise the performance will suffer. + * If no element is found, then a TargetError is thrown. + * @param condition The condition to filter with, or a callback function. + * The condition object additionally supports named parameters. + * Default MatchMode is "Exact", and CaseSense "On". + * See a more detailed explanation under FindElement condition argument. + * + * There is a difference only if a callback function is used. With a cached search a treewalker is + * not used (instead CachedChildren is accessed), and the `index` argument in the callback function + * will receive the actual index of the element relative to the parent, even if order = LastToFirst. + * @param scope Optional TreeScope value: Element, Children, Family (Element+Children), Descendants, Subtree (=Element+Descendants). Default is Descendants. + * @param index Looks for the n-th element matching the condition + * @param order Optional: custom tree navigation order, one of UIA.TreeTraversalOptions values (LastToFirstOrder, PostOrder, LastToFirstPostOrder). Default is FirstToLast and PreOrder. + * @param startingElement Optional: search will start from this element instead, which must be a child/descendant of the starting element + * If startingElement is supplied then part of the tree will not be searched (depending on TreeTraversalOrder, either everything before this element, or everything after it will be ignored) + * Unlike FindElement, using this will most likely not have a beneficial effect on performance. + * @returns {UIA.IUIAutomationElement} + */ + FindCachedElement(condition, scope:=4, index:=1, order:=0, startingElement:=0) { + local callback := 0, found, c := 0 + condition := UIA.IUIAutomationElement.__ExtractConditionNamedParameters(condition, &scope, &order, &startingElement, , &index, &callback) + callback := callback || UIA.IUIAutomationElement.Prototype.ValidateCondition.Bind(unset, condition, true), scope := UIA.TypeValidation.TreeScope(scope), index := UIA.TypeValidation.Integer(index, "Index"), order := UIA.TypeValidation.TreeTraversalOptions(order), startingElement := UIA.TypeValidation.Element(startingElement) + if index < 0 + order |= 2, index := -index + else if index = 0 + throw ValueError("Condition index cannot be 0", -1) + if startingElement + startingElement := startingElement.RuntimeId + ChildOrder := order&2 ? "ReversedCachedChildren" : "CachedChildren" + ; First handle PostOrder + if order&1 { + if found := PostOrderRecursiveFind(this, scope) + return found + throw TargetError("An element matching the condition was not found", -1) + } + ; PreOrder + if scope&1 && (c := callback(this, 1, 0)) > 0 && --index = 0 + return this + if scope > 1 && c >= 0 { + if found := PreOrderRecursiveFind(this) + return found + } + throw TargetError("An element matching the condition was not found", -1) + PreOrderRecursiveFind(el, depth:=0) { + local child, found, c, i + depth++ + for i, child in el.%ChildOrder% { + c := 0 + if (startingElement ? (startingElement = child.RuntimeId ? !(startingElement := "") && (scope & 1) : 0) : 1) && (c := callback(child, i, depth)) > 0 && --index = 0 + return child + else if (c >= 0) && scope&4 && (found := PreOrderRecursiveFind(child, depth)) + return found + } + } + PostOrderRecursiveFind(el, scope, i:=1, depth:=-1) { + local child, found, c := 0, j + depth++ + for j, child in el.%ChildOrder% { + if IsObject(found := PostOrderRecursiveFind(child, (scope & ~2)|1, j, depth)) + return found + else if found = -1 + break + } + if scope&1 && (startingElement ? (startingElement = el.RuntimeId ? !(startingElement := "") && (scope & 1) : 0) : 1) && (c := callback(el, i, depth)) > 0 && --index = 0 + return el + return c + } + } + + /** + * Returns all UI Automation elements that satisfy the specified condition. + * @param condition The condition to filter with, or a callback function. + * The condition object additionally supports named parameters. + * Default MatchMode is "Exact", and CaseSense "On". + * See a more detailed explanation under FindElement condition argument. + * @param scope Optional TreeScope value: Element, Children, Family (Element+Children), Descendants, Subtree (=Element+Descendants). Default is Descendants. + * @param order Optional: custom tree navigation order, one of UIA.TreeTraversalOptions values (LastToFirstOrder, PostOrder, LastToFirstPostOrder). Default is FirstToLast and PreOrder. [requires Windows 10 version 1703+] + * @param startingElement Optional: element with which to begin the search [requires Windows 10 version 1703+] + * @param cacheRequest Optional: cache request object + * @returns {[UIA.IUIAutomationElement]} + */ + FindElements(condition, scope := 4, order:=0, startingElement:=0, cacheRequest:=0) { + local withOptions, found, cache, callback := 0 + condition := UIA.IUIAutomationElement.__ExtractConditionNamedParameters(condition, &scope, &order, &startingElement, &cacheRequest,, &callback) + scope := UIA.TypeValidation.TreeScope(scope), order := UIA.TypeValidation.TreeTraversalOptions(order), startingElement := UIA.TypeValidation.Element(startingElement), cacheRequest := UIA.TypeValidation.CacheRequest(cacheRequest) + withOptions := order || startingElement + if withOptions && !UIA.IsIUIAutomationElement7Available && (Type(condition) ~= "i)Condition|ComValue") + throw ValueError("If using order or startingElement arguments in Windows <10.0.15063, then condition cannot be IUIAutomationCondition", -1) + if !(Type(condition) ~= "i)Condition|ComValue") { + IUIAcondition := UIA.__ConditionBuilder(condition, &nonUIAEncountered:=0), unfilteredEls := [], filteredEls := [] + + if callback { + local foundElements := [], TW := UIA.CreateTreeWalker(IUIAcondition), c := 0 + if startingElement + startingElement := startingElement.RuntimeId + ; FirstToLast or LastToFirst + if order&2 + nextElementFunc := UIA.IUIAutomationTreeWalker.Prototype.TryGetPreviousSiblingElement.Bind(TW), + nextChildFunc := UIA.IUIAutomationTreeWalker.Prototype.TryGetLastChildElement.Bind(TW) + else + nextElementFunc := UIA.IUIAutomationTreeWalker.Prototype.TryGetNextSiblingElement.Bind(TW), + nextChildFunc := UIA.IUIAutomationTreeWalker.Prototype.TryGetFirstChildElement.Bind(TW) + ; First handle PostOrder + if order&1 + return (PostOrderRecursiveFind(this, scope), foundElements) + ; PreOrder + if scope&1 && (c := callback(this)) > 0 + foundElements.Push(cacheRequest ? this.BuildUpdatedCache(cacheRequest) : this) + if scope > 1 && c >= 0 + PreOrderRecursiveFind(nextChildFunc(this)) + return foundElements + } + + if nonUIAEncountered = 2 ; A cached name was encountered, use FindCachedElements instead + return this.FindCachedElements(condition, scope, order, startingElement) + else if nonUIAEncountered = 1 || (withOptions && (!UIA.IsIUIAutomationElement7Available || (scope < 4 && startingElement))) { + cache := this.BuildUpdatedCache(UIA.CreateCacheRequest(condition,,scope,,scope>3 ? IUIAcondition : unset)) + found := cache.FindCachedElements(condition, scope, order, startingElement) + if cacheRequest { + Loop found.Length + found[A_Index] := found[A_Index].BuildUpdatedCache(cacheRequest) + } + return found + } + unfilteredCondition := condition, condition := IUIAcondition + } + if !startingElement ; If FindFirstWithOptions is used but startingElement isn't set, start from this element. + startingElement := this + if cacheRequest { + if withOptions + ComCall(113, startingElement, "int", scope, "ptr", condition, "ptr", cacheRequest, "int", order, "ptr", this, "ptr*", &found := 0) ; FindAllWithOptionsBuildCache + else + ComCall(8, this, "int", scope, "ptr", condition, "ptr", cacheRequest, "ptr*", &found := 0) ; FindAllBuildCache + } else { + if withOptions + ComCall(111, startingElement, "int", scope, "ptr", condition, "int", order, "ptr", this, "ptr*", &found := 0) ; FindAllWithOptions + else + ComCall(6, this, "int", scope, "ptr", condition, "ptr*", &found := 0) ; FindAll + } + return found ? UIA.IUIAutomationElementArray(found).ToArray() : [] + + PreOrderRecursiveFind(el, depth:=0) { + ++depth + while (c := 0, IsObject(el)) { + if (startingElement ? (startingElement = el.RuntimeId ? !(startingElement := "") && (scope & 1) : 0) : 1) && (c := callback(el, A_Index, depth)) > 0 + foundElements.Push(cacheRequest ? el.BuildUpdatedCache(cacheRequest) : el) + if scope&4 && c >= 0 + PreOrderRecursiveFind(nextChildFunc(el), depth) + el := nextElementFunc(el) + } + } + PostOrderRecursiveFind(el, scope, i:=1, depth:=-1) { ; this is only called if scope >= 4, and handles scope&2 as well + local child := nextChildFunc(el), c := 0 + ++depth + if scope > 1 { + while (IsObject(child)) { + if PostOrderRecursiveFind(child, (scope & ~2)|1, A_Index, depth) = -1 + break + child := nextElementFunc(child) + } + } + if (startingElement ? (startingElement = el.RuntimeId ? !(startingElement := "") && (scope & 1) : 0) : 1) && (c := callback(el)) > 0 + foundElements.Push(cacheRequest ? el.BuildUpdatedCache(cacheRequest) : el) + return c + } + } + + /** + * Returns all UI Automation elements that satisfy the specified condition inside a cached tree, checking only cached properties. + * This is not a UIA native method: make sure the cached tree is reasonably small, otherwise the performance will suffer. + * @param condition The condition to filter with, or a callback function. + * The condition object additionally supports named parameters. + * Default MatchMode is "Exact", and CaseSense "On". + * See a more detailed explanation under FindElement condition argument. + * @param scope Optional TreeScope value: Element, Children, Family (Element+Children), Descendants, Subtree (=Element+Descendants). Default is Descendants. + * @param order Optional: custom tree navigation order, one of UIA.TreeTraversalOptions values (LastToFirstOrder, PostOrder, LastToFirstPostOrder). Default is FirstToLast and PreOrder. + * @param startingElement Optional: element with which to begin the search + * Unlike FindElements, using this will not give a performance benefit. + * @returns {[UIA.IUIAutomationElement]} + */ + FindCachedElements(condition, scope:=4, order:=0, startingElement:=0) { + local callback := 0, foundElements := [], c := 0 + condition := UIA.IUIAutomationElement.__ExtractConditionNamedParameters(condition, &scope, &order, &startingElement,,, &callback) + callback := callback || UIA.IUIAutomationElement.Prototype.ValidateCondition.Bind(unset, condition, true), scope := UIA.TypeValidation.TreeScope(scope), order := UIA.TypeValidation.TreeTraversalOptions(order), startingElement := UIA.TypeValidation.Element(startingElement) + if startingElement + startingElement := startingElement.RuntimeId + ChildOrder := order&2 ? "ReversedCachedChildren" : "CachedChildren" + ; First handle PostOrder + if order&1 + return (PostOrderRecursiveFind(this, scope), foundElements) + ; PreOrder + if scope&1 && (c := callback(this, 1, 0)) > 0 + foundElements.Push(this) + if scope > 1 && c >= 0 + return (PreOrderRecursiveFind(this), foundElements) + return foundElements + + PreOrderRecursiveFind(el, depth:=0) { + local child, c, i + depth++ + for i, child in el.%ChildOrder% { + c := 0 + if (startingElement ? (startingElement = child.RuntimeId ? !(startingElement := "") && (scope & 1) : 0) : 1) && (c := callback(child, i, depth)) > 0 + foundElements.Push(child) + if c >= 0 && scope&4 + PreOrderRecursiveFind(child, depth) + } + } + PostOrderRecursiveFind(el, scope, i:=1, depth:=-1) { + local child, c := 0, j + depth++ + if scope > 1 { + for j, child in el.%ChildOrder% { + if PostOrderRecursiveFind(child, (scope & ~2)|1, j, depth) = -1 + break + } + } + if scope&1 && (startingElement ? (startingElement = el.RuntimeId ? !(startingElement := "") && (scope & 1) : 0) : 1) && (c := callback(el, i, depth)) > 0 + foundElements.Push(el) + return c + } + } + + /** + * Wait element to exist. + * @param condition The condition to filter with. + * The condition object additionally supports named parameters. + * See a more detailed explanation under FindElement condition argument. + * @param timeOut Waiting time for element to appear in ms. Default: indefinite wait + * @param scope Optional TreeScope value: Element, Children, Family (Element+Children), Descendants, Subtree (=Element+Descendants). Default is Descendants. + * @param index Looks for the n-th element matching the condition + * @param order Optional: custom tree navigation order, one of UIA.TreeTraversalOptions values (LastToFirstOrder, PostOrder, LastToFirstPostOrder) [requires Windows 10 version 1703+] + * @param startingElement Optional: element with which to begin the search [requires Windows 10 version 1703+] + * @param cacheRequest Optional: cache request object + * @param tick Optional: sleep time between checks for the element. Default is 20ms + * @returns {UIA.IUIAutomationElement} Found element if successful, 0 if timeout happens + */ + WaitElement(condition, timeOut := -1, scope := 4, index := 1, order := 0, startingElement := 0, cacheRequest := 0, tick := 20) { + local endtime + if Type(condition) = "Object" { + condition := condition.Clone() + timeOut := (condition.DeleteProp("timeOut") || timeOut), tick := (condition.DeleteProp("tick") || tick) + } + timeOut := UIA.TypeValidation.Integer(timeOut, "TimeOut"), tick := UIA.TypeValidation.Integer(tick, "Tick") + endtime := A_TickCount + timeOut + While ((timeOut == -1) || (A_Tickcount < endtime)) { + try return this.FindElement(condition, scope, index, order, startingElement, cacheRequest) + Sleep tick + } + return 0 + } + + /** + * Wait element to not exist (disappear). + * @param condition The condition to filter with. + * The condition object additionally supports named parameters. + * See a more detailed explanation under FindElement condition argument. + * @param timeout Waiting time for element to disappear. Default: indefinite wait + * @param scope Optional TreeScope value: Element, Children, Family (Element+Children), Descendants, Subtree (=Element+Descendants). Default is Descendants. + * @param order Optional: custom tree navigation order, one of UIA.TreeTraversalOptions values (LastToFirstOrder, PostOrder, LastToFirstPostOrder) [requires Windows 10 version 1703+] + * @param startingElement Optional: element with which to begin the search [requires Windows 10 version 1703+] + * @param cacheRequest Optional: cache request object + * @param tick Optional: sleep time between checks for the element. Default is 20ms + * @returns 1 if element disappeared, 0 otherwise + */ + WaitElementNotExist(condition, timeout := -1, scope := 4, index := 1, order := 0, startingElement := 0, cacheRequest := 0, tick := 20) { + local endtime + if Type(condition) = "Object" { + condition := condition.Clone() + timeOut := (condition.DeleteProp("timeOut") || timeOut), tick := (condition.DeleteProp("tick") || tick) + } + timeOut := UIA.TypeValidation.Integer(timeOut, "TimeOut"), tick := UIA.TypeValidation.Integer(tick, "Tick") + endtime := A_TickCount + timeout + While (timeout == -1) || (A_Tickcount < endtime) { + try this.FindElement(condition, scope, index, order, startingElement, cacheRequest) + catch + return 1 + Sleep tick + } + return 0 + } + + /** + * Tries to get an element from a path. If no element is found, an IndexError is thrown. + * `ElementFromPath(path1[, path2, ...])` + * + * Paths can be: + * 1. Comma-separated numeric path that defines which path to travel down the tree. In addition + * to integer values, or TypeN which selects the nth occurrence of Type. + * Eg. `Element.ElementFromPath("3,2")` => selects Elements third childs second child + * `Element.ElementFromPath("Button3,2")` => selects Elements third child of type Button, then its second child + * + * 2. UIA path copied from UIAViewer. + * Eg. `Element.ElementFromPath("bAx3")` + * + * 3. A condition or conditions. In this case the provided conditions define the route of tree-traversal, by default with Scope Children. + * Eg. `Element.ElementFromPath({Type:"Button"}, {Type:"List"})` => finds the first Button type child of Element, then the first List type child of that element + * + * @returns {UIA.IUIAutomationElement} + */ + ElementFromPath(paths*) { + local err + try return this[paths*] + catch IndexError as err + throw IndexError(StrReplace(err.Message, "at index", "at path index"), -1, err.Extra) + } + + /** + * Tries to get a cached element from a path. If no element is found, an IndexError is thrown. + * This method uses only cached elements and cached properties to perform the search. + * `CachedElementFromPath(path1[, path2, ...])` + * + * Paths can be: + * 1. Comma-separated numeric path that defines which path to travel down the tree. In addition + * to integer values, or TypeN which selects the nth occurrence of Type. + * Eg. `Element.CachedElementFromPath("3,2")` => selects Elements third childs second child + * `Element.CachedElementFromPath("Button3,2")` => selects Elements third child of type Button, then its second child + * + * 2. UIA path copied from UIAViewer. + * Eg. `Element.CachedElementFromPath("bAx3")` + * + * 3. A condition or conditions. In this case the provided conditions define the route of tree-traversal, by default with Scope Children. + * Eg. `Element.CachedElementFromPath({Type:"Button"}, {Type:"List"})` => finds the first Button type child of Element, then the first List type child of that element + * + * @returns {UIA.IUIAutomationElement} + */ + CachedElementFromPath(paths*) { + local el := this, _, path, subpath, arr, m + for _, path in paths { + if IsObject(path) { + try el := el.FindCachedElement(path, 2) + catch + el := "" + } else if IsInteger(path) { + try { + arr := el.CachedChildren + el := arr[path < 0 ? arr.Length+path+1 : path] + } catch + el := "" + } else if path is String { + maybeEl := "" + for subpath in StrSplit(path, "|") { + try { + if InStr(subpath, ".") || InStr(subpath, ",") + maybeEl := el.CachedElementFromPath(StrSplit(StrReplace(subpath, ".", ","), ",")*) + else if RegexMatch(subpath, "i)([a-zA-Z]+) *(\d+)?", &m:="") && UIA.Type.HasOwnProp(m[1]) { + try maybeEl := el.FindCachedElement({Type:m[1], i:m[2]}, 2) + } else if !(subpath ~= ",\.+-") + maybeEl := el.CachedElementFromPath(UIA.DecodePath(subpath)*) + else + continue + break ; no errors encountered means that match was found + } + } + el := maybeEl + } else + throw TypeError("Invalid item type at index " _, -1) + if !el + throw IndexError("Invalid index/condition at index " _, -1) + } + return el + } + + /** + * Checks whether an element exists at a path and returns the element if one is found. + * If no element is found, 0 is returned. + * `ElementFromPathExist(path1[, path2, ...])` + * + * Paths can be: + * 1. Comma-separated numeric path that defines which path to travel down the tree. In addition + * to integer values, or TypeN which selects the nth occurrence of Type. + * Eg. `Element.ElementFromPathExist("3,2")` => checks for Elements third childs second child + * `Element.ElementFromPathExist("Button3,2")` => checks for Elements third child of type Button, then its second child + * + * 2. UIA path copied from UIAViewer. + * Eg. `Element.ElementFromPathExist("bAx3")` + * + * 3. A condition or conditions. In this case the provided conditions define the route of tree-traversal, by default with Scope Children. + * Eg. `Element.ElementFromPathExist({Type:"Button"}, {Type:"List"})` => checks for the first Button type child of Element, then the first List type child of that element + * + * @returns {UIA.IUIAutomationElement} + */ + ElementFromPathExist(paths*) { + try return this[paths*] + catch IndexError + return 0 + return 0 + } + + /** + * Checks whether a cached element exists at a path and returns the cached element if one is found. + * This method uses only cached elements and cached properties to perform the search. + * If no element is found, 0 is returned. + * + * @param paths Either an UIA path, numeric path, or condition path. + * See longer explanation under CachedElementFromPath + * + * @returns {UIA.IUIAutomationElement} + */ + CachedElementFromPathExist(paths*) { + try return this.CachedElementFromPath(paths*) + catch IndexError + return 0 + return 0 + } + + /** + * Wait element to appear at a path. The last argument may optionally be a timeout in milliseconds (default is indefinite wait). + * `WaitElementFromPath(path1[, path2, ..., timeout])` + * + * Paths can be: + * 1. Comma-separated numeric path that defines which path to travel down the tree. In addition + * to integer values, or TypeN which selects the nth occurrence of Type. + * Eg. `Element.WaitElementFromPath("3,2", 2000)` => waits for Elements third childs second child, timeout 2 seconds + * `Element.WaitElementFromPath("Button3,2")` => waits for Elements third child of type Button, then its second child + * + * 2. UIA path copied from UIAViewer. + * Eg. `Element.WaitElementFromPath("bAx3")` + * + * 3. A condition or conditions. In this case the provided conditions define the route of tree-traversal, by default with Scope Children. + * Eg. `Element.WaitElementFromPath({Type:"Button"}, {Type:"List"})` => waits for the first Button type child of Element, then the first List type child of that element + * + * @returns {UIA.IUIAutomationElement} + */ + WaitElementFromPath(paths*) { + local timeOut := -1, endtime, tick := 20 + if paths.Length > 1 && paths[paths.Length] is Integer { + paths := paths.Clone() + if paths[-2] is Integer + tick := paths.Pop() + timeOut := paths.Pop() + } + endtime := A_TickCount + timeOut + While ((timeOut == -1) || (A_Tickcount < endtime)) { + try return this[paths*] + Sleep tick + } + } + + /** + * Traverses the tree purely using a TreeWalker that is created with an optional filter-condition (default is TrueCondition) + * @param searchPath This is a comma-separated path that defines the route of tree traversal: + * * n: gets the nth child (using GetFirstChildElement and GetNextSiblingElement) + * * +n: gets the nth next sibling + * * -n: gets the nth previous sibling + * * pn: gets the nth parent + * * "n": normalizes the element (returns the first parent or the element itself matching the filterCondition) + * @param filterCondition Optional: a condition for tree traversal that selects only elements that match the condition. + * @example Element.WalkTree("p,+2,1") ; gets the parent of Element, then the second sibling of the parent, then that siblings first child. + * @returns {UIA.IUIAutomationElement} + */ + WalkTree(searchPath, filterCondition?) { + local PathTW, el, loopFunc, m, err + PathTW := IsSet(filterCondition) ? UIA.CreateTreeWalker(filterCondition) : UIA.TreeWalkerTrue + el := this, searchPath := StrReplace(StrReplace(String(searchPath), " "), ".", ",") + Loop Parse searchPath, "," { + try { + if IsDigit(A_LoopField) { + loopFunc := "GetFirstChildElement", el := PathTW.GetFirstChildElement(el) + if A_LoopField > 1 + el := el.WalkTree("+" A_LoopField-1) + } else if A_LoopField = "n" { + loopFunc := "NormalizeElement", el := PathTW.NormalizeElement(el) + } else if RegexMatch(A_LoopField, "i)([p+-]+) *(\d+)?", &m:="") { + if (loopFunc := (m[1] = "p") ? "GetParentElement" : (m[1] = "+") ? "GetNextSiblingElement" : (m[1] = "-") ? "GetPreviousSiblingElement" : "") { + Loop m[2] || 1 { + el := PathTW.%loopFunc%(el) + } + } else + throw ValueError() + } else + throw ValueError() + } catch TargetError, IndexError { + throw IndexError("Step " A_index " with value " A_LoopField " was out of bounds (" loopFunc " failed)", -1) + } catch ValueError { + throw ValueError("Invalid path value " A_LoopField " at step " A_index, -1) + } catch OSError as err { + throw ValueError("Step " A_index " with value " A_LoopField " caused COM error: " err.Message, -1, "Make sure the element is valid and not cached (use WalkCachedTree for cached elements).") + } catch Any as err + throw + } + return el + } + + /** + * Traverses the cached tree, optionally filtering by filterCondition + * @param searchPath This is a comma-separated path that defines the route of tree traversal: + * * n: gets the nth child (using GetFirstChildElement and GetNextSiblingElement) + * * +n: gets the nth next sibling + * * -n: gets the nth previous sibling + * * pn: gets the nth parent + * * "n": normalizes the element (returns the first parent or the element itself matching the filterCondition) + * @param filterCondition Optional: a condition for tree traversal that selects only elements that match the condition. + * @example Element.WalkCachedTree("p,+2,1") ; gets the parent of Element, then the second sibling of the parent, then that siblings first child. + * @returns {UIA.IUIAutomationElement} + */ + WalkCachedTree(searchPath, filterCondition?) { + local el, m, parent, children, child, err, steps + el := this, searchPath := StrReplace(StrReplace(String(searchPath), " "), ".", ",") + Loop Parse searchPath, "," { + try { + if IsDigit(A_LoopField) { + el := el.CachedChildren[A_LoopField] + } else if A_LoopField = "n" { + if !IsSet(filterCondition) + throw Error("Normalizing an element requires a filterCondition", -1) + if !this.ValidateCondition(filterCondition, true) { + if !(parent := el.CachedParent) + throw TargetError() + el := parent.WalkCachedTree("n", filterCondition) + } + } else if RegexMatch(A_LoopField, "i)([p+-]+) *(\d+)?", &m:="") { + if (m[1] = "p") { + counter := 0, steps := m[2] || 1 + Loop { + if !(el := el.CachedParent) + throw TargetError() + if IsSet(filterCondition) && !el.ValidateCondition(filterCondition, true) + continue + if ++counter = steps + break + } + } else if (m[1] = "+") || (m[1] = "-") { + parent := el.CachedParent, children := parent.CachedChildren + if !parent + throw TargetError() + if IsSet(filterCondition) { + startingIndex := GetIndex(parent), counter := 0, steps := Integer(m[2] || 1) + Loop (m[1] = "+") ? children.Length - startingIndex : startingIndex - 1 { + child := children[startingIndex + ((m[1] = "+") ? A_index : -A_index)] + if child.ValidateCondition(filterCondition, true) && ++counter = steps { + el := child + break + } + RecurseTree(child) + if counter = steps + break + } + if counter != steps + el := "" + } else + el := parent[GetIndex(parent) + ((m[1] = "+") ? (m[2] || 1) : -(m[2] || 1))] + } else + throw ValueError() + } + } catch TargetError, IndexError { + throw IndexError("Step " A_index " with value " A_LoopField " was out of bounds", -1) + } catch ValueError { + throw ValueError("Invalid path value " A_LoopField " at step " A_index, -1) + } catch OSError as err { + throw ValueError("Step " A_index " with value " A_LoopField " caused COM error: " err.Message, -1, "Either CachedParent or CachedChildren failed: make sure the target and the targets parent are cached.") + } catch Any as err + throw + if !el + throw IndexError("No element found after step " A_index " with value " A_LoopField, -1) + } + return el + GetIndex(parent) { + local elId, i, child + elId := el.RuntimeId + for i, child in parent.CachedChildren { + if child.RuntimeId = elId + return i + } + } + RecurseTree(parent) { + local children := parent.CachedChildren, child + if (m[1] = "+") { + for child in children { + if child.ValidateCondition(filterCondition, true) && ++counter = steps + return el := child + if RecurseTree(child) + return + } + } else { + Loop children.Length { + child := children[children.Length-A_Index+1] + if child.ValidateCondition(filterCondition, true) && ++counter = steps + return el := child + if RecurseTree(child) + return + } + } + } + } + + ; Gets all property values of this element and returns an object where Object.PropertyName = PropertyValue + ; Use only for debugging purposes, since this will be quite slow. + GetAllPropertyValues() { + local infos := {}, k, v, arr, t + for k, v in UIA.Property.OwnProps() { + if k = "__CachedValues" + continue + v := this.GetPropertyValue(v) + if (v is ComObjArray) { + arr := [] + for t in v + arr.Push(t) + v := arr + } + infos.%k% := v + } + return infos + } + + ; Internal method: checks whether this element matches the condition + ValidateCondition(cond, cached:=False, i?, depth?) { + local mm, cs, notCond := 0, k, v, result, val + if !IsObject(cond) + throw ValueError("Condition of type " Type(cond) " is an invalid ValidateCondition condition", -1, "The condition can be a plain object, array or a method") + if HasMethod(cond) + return cond(this, i?, depth?) + if Type(cond) = "Object" { + mm := cond.HasOwnProp("matchmode") ? cond.matchmode : cond.HasOwnProp("mm") ? cond.mm : unset + cs := cond.HasOwnProp("casesense") ? cond.casesense : cond.HasOwnProp("cs") ? cond.cs : unset + if !IsInteger(mm ?? 0) + mm := UIA.MatchMode.%mm% + if !IsInteger(cs ?? 0) + cs := UIA.CaseSense.%cs% + notCond := cond.HasOwnProp("operator") ? cond.operator = "not" : cond.HasOwnProp("op") ? cond.op = "not" : 0 + cond := cond.OwnProps() + } else if cond is Array { + for k, v in cond { + if IsObject(v) && this.ValidateCondition(v, cached, i?, depth?) + return true + } + return false + } else + throw ValueError("Condition of type " Type(cond) " is an invalid ValidateCondition condition", -1, "The condition can be a plain object, array or a method") + + for k, v in cond { + try k := UIA.TypeValidation.Property(k) + if !(IsInteger(k) && k >= 10000) { + if IsObject(v) { + if (k = "not" ? this.ValidateCondition(v, cached, i?, depth?) : !this.ValidateCondition(v, cached, i?, depth?)) + return False + } + continue + } + if k = 30003 + v := UIA.TypeValidation.Type(v) + try currentValue := this.Get%cached ? "Cached" : ""%PropertyValue(k) + catch { ; If caching is used, should this throw an error to notify the user of a missing cache property? + result := 0 + } else { + switch UIA.PropertyVariantType[k] { + case 8: + if v is Array { + for val in v { + if (result := CompareStrings(currentValue, val, mm?, cs?)) + break + } + } else + result := CompareStrings(currentValue, v, mm?, cs?) + case 3,5,11: + if v is Array { + for val in v { + if k = 30003 + val := UIA.TypeValidation.Type(val) + if (result := currentValue == val) + break + } + } else + result := currentValue == v + default: + if Type(v) = "Object" + v := v.OwnProps(), currentValue := currentValue is Array ? currentValue : currentValue.OwnProps() + if not v is Array { + if k = 30000 { + local v := StrSplit(v, ".") + for t in v + v[A_Index] := Integer("0x" t) + } else + throw ValueError("Invalid PropertyId " k, -1) + } + if result := currentValue.Length == v.Length { + for i, val in v { + try { + if currentValue[i] == val + continue + } + result := 0 + break + } + } + } + } + if notCond ? result : !result + return False + } + return True + + CompareStrings(str1, str2, matchmode:=3, casesense:=-1) { + if not str1 is String + str1 := String(str1) + if not str2 is String + str2 := String(str2) + if matchmode = 3 + return (casesense ? str1 == str2 : str1 = str2) + else if matchmode = 2 + return InStr(str1, str2, !!casesense) + if matchmode = 1 + return ((casesense && SubStr(str1, 1, StrLen(str2)) == str2) || (!casesense && SubStr(str1, 1, StrLen(str2)) = str2)) + else if matchmode = "RegEx" { + if casesense = -1 + return RegExMatch(str1, str2) + return RegExMatch(str1, (!casesense && !RegExMatch(str2, "^([^(\\]+)\)")) ? "i)" str2 : str2) + } else + throw Error("Invalid MatchMode", -1, matchmode) + } + } + + ; Sets the keyboard focus to this UI Automation element. + SetFocus() => ComCall(3, this) + + ; Retrieves the unique identifier assigned to the UI element. + ; The identifier is only guaranteed to be unique to the UI of the desktop on which it was generated. Identifiers can be reused over time. + ; The format of run-time identifiers might change in the future. The returned identifier should be treated as an opaque value and used only for comparison; for example, to determine whether a Microsoft UI Automation element is in the cache. + GetRuntimeId() { + local runtimeId + return (ComCall(4, this, "ptr*", &runtimeId := 0), UIA.SafeArrayToAHKArray(runtimeId)) + } + + /** + * Retrieves the first child or descendant element that matches the specified condition. + * The scope of the search is relative to the element on which the method is called. Elements are returned in the order in which they are encountered in the tree. + * This function cannot search for ancestor elements in the UIA tree; that is, UIA.TreeScope.Ancestors is not a valid value for the scope parameter. + * When searching for top-level windows on the desktop, be sure to specify UIA.TreeScope.Children in the scope parameter, not UIA.TreeScope.Descendants. A search through the entire subtree of the desktop could iterate through thousands of items and lead to a stack overflow. + * @param condition The condition to filter with. MatchModes StartsWith, RegEx, and using indexes are not supported with FindFirst, use FindElement for that. + * @param scope Optional TreeScope value: Element, Children, Family (Element+Children), Descendants, Subtree (=Element+Descendants). Default is Descendants. + * @returns {UIA.IUIAutomationElement} + */ + FindFirst(condition := UIA.TrueCondition, scope:=4) { + local found + scope := UIA.TypeValidation.TreeScope(scope) + if !(Type(condition) ~= "i)Condition|ComValue") + condition := UIA.__CreateRawCondition(condition) + if (ComCall(5, this, "int", scope, "ptr", condition ?? UIA.TrueCondition, "ptr*", &found := 0), found) + return UIA.IUIAutomationElement(found) + throw TargetError("An element matching the condition was not found", -1) + } + + /** + * Returns all UI Automation elements that satisfy the specified condition. + * The scope of the search is relative to the element on which the method is called. Elements are returned in the order in which they are encountered in the tree. + * This function cannot search for ancestor elements in the UIA tree; that is, UIA.TreeScope.Ancestors is not a valid value for the scope parameter. + * When searching for top-level windows on the desktop, be sure to specify UIA.TreeScope.Children in the scope parameter, not UIA.TreeScope.Descendants. A search through the entire subtree of the desktop could iterate through thousands of items and lead to a stack overflow. + * @param condition The condition to filter with. + * @param scope Optional TreeScope value: Element, Children, Family (Element+Children), Descendants, Subtree (=Element+Descendants). Default is Descendants. + * @returns {[UIA.IUIAutomationElement]} + */ + FindAll(condition := UIA.TrueCondition, scope := 4) { + local found + if !IsObject(condition) + throw TypeError("Condition must be an condition object or array", -1) + if !(Type(condition) ~= "i)Condition|ComValue") + condition := UIA.__CreateRawCondition(condition) + return (ComCall(6, this, "int", UIA.TypeValidation.TreeScope(scope), "ptr", condition, "ptr*", &found := 0), found) ? UIA.IUIAutomationElementArray(found).ToArray() : [] + } + + ; Retrieves the first child or descendant element that matches the specified condition, prefetches the requested properties and control patterns, and stores the prefetched items in the cache. + FindFirstBuildCache(cacheRequest, condition := UIA.TrueCondition, scope := 4) { + local found + if !(Type(condition) ~= "i)Condition|ComValue") + condition := UIA.__CreateRawCondition(condition) + if (ComCall(7, this, "int", UIA.TypeValidation.TreeScope(scope), "ptr", condition, "ptr", UIA.TypeValidation.CacheRequest(cacheRequest), "ptr*", &found := 0), found) + return UIA.IUIAutomationElement(found) + throw TargetError("An element matching the condition was not found", -1) + } + + ; Returns all UI Automation elements that satisfy the specified condition, prefetches the requested properties and control patterns, and stores the prefetched items in the cache. + FindAllBuildCache(cacheRequest, condition := UIA.TrueCondition, scope := 4) { + local found + if !(Type(condition) ~= "i)Condition|ComValue") + condition := UIA.__CreateRawCondition(condition) + return (ComCall(8, this, "int", UIA.TypeValidation.TreeScope(scope), "ptr", condition, "ptr", UIA.TypeValidation.CacheRequest(cacheRequest), "ptr*", &found := 0), found) ? UIA.IUIAutomationElementArray(found).ToArray() : [] + } + + ; Retrieves a UI Automation element with an updated cache. + ; The original UI Automation element is unchanged. The UIA.IUIAutomationElement interface refers to the same element and has the same runtime identifier. + BuildUpdatedCache(cacheRequest) { + local updatedElement + return (ComCall(9, this, "ptr", UIA.TypeValidation.CacheRequest(cacheRequest), "ptr*", &updatedElement := 0), UIA.IUIAutomationElement(updatedElement)) + } + + ; Microsoft UI Automation properties of the double type support Not a Number (NaN) values. When retrieving a property of the double type, a client can use the _isnan function to determine whether the property is a NaN value. + + ; Retrieves the current value of a property for this UI Automation element. + ; This gets called when Element.Property is used + GetPropertyValue(propertyId) { + local val + if !IsNumber(propertyId) { + if UIA.Property.HasOwnProp(propertyId) + propertyId := UIA.Property.%propertyId% + else + throw ValueError("Invalid propertyId.", -1, IsObject(propertyId) ? "PropertyId must be a string or an integer, but " Type(propertyId) " was provided" : propertyId) + } + ComCall(10, this, "int", propertyId, "ptr", val := UIA.ComVar()) + return val[] is ComObjArray ? UIA.SafeArrayToAHKArray(val[]) : val[] + } + + ; Retrieves a property value for this UI Automation element, optionally ignoring any default value. + ; Passing FALSE in the ignoreDefaultValue parameter is equivalent to calling UIA.IUIAutomationElement.GetPropertyValue. + ; If the Microsoft UI Automation provider for the element itself supports the property, the value of the property is returned. Otherwise, if ignoreDefaultValue is FALSE, a default value specified by UI Automation is returned. + ; This method returns a failure code if the requested property was not previously cached. + GetPropertyValueEx(propertyId, ignoreDefaultValue) { + local val + ComCall(11, this, "int", UIA.TypeValidation.Property(propertyId), "int", ignoreDefaultValue, "ptr", val := UIA.ComVar()) + return val[] is ComObjArray ? UIA.SafeArrayToAHKArray(val[]) : val[] + } + + ; Retrieves a property value from the cache for this UI Automation element. + ; This is called when Element.CachedProperty is used (eg Element.CachedName) + GetCachedPropertyValue(propertyId) { + local val + ComCall(12, this, "int", UIA.TypeValidation.Property(propertyId), "ptr", val := UIA.ComVar()) + return val[] is ComObjArray ? UIA.SafeArrayToAHKArray(val[]) : val[] + } + + ; Retrieves a property value from the cache for this UI Automation element, optionally ignoring any default value. + GetCachedPropertyValueEx(propertyId, ignoreDefaultValue, retVal) { + local val + ComCall(13, this, "int", UIA.TypeValidation.Property(propertyId), "int", ignoreDefaultValue, "ptr", val := UIA.ComVar()) + return val[] is ComObjArray ? UIA.SafeArrayToAHKArray(val[]) : val[] + } + + ; Retrieves the control pattern interface of the specified pattern on this UI Automation element. + ; For riid specify a GUID string, that is the IID for the desired pattern + ; GetPatternAs doesn't have a use in this library, use GetPattern instead + GetPatternAs(patternId, riid) { + local name, GUID + try { + if IsInteger(patternId) + name := UIA.Pattern[patternId] + else + patternId := UIA.Pattern.%(name := patternId)% + } catch + Throw ValueError("Failed to find pattern `"" name "`" among UIA.Pattern properties/values", -1) + DllCall("ole32\CLSIDFromString", "wstr", riid, "ptr", GUID:=Buffer(16)) + ComCall(14, this, "int", patternId, "ptr", GUID, "ptr*", &patternObject := 0) + return UIA.IUIAutomation%name%Pattern(patternObject) + } + + ; Retrieves the control pattern interface of the specified pattern from the cache of this UI Automation element. + ; GetCachedPatternAs doesn't have a use in this library, use GetCachedPattern instead + GetCachedPatternAs(patternId, riid) { ; not completed + local name, GUID, patternObject + try { + if IsInteger(patternId) + name := UIA.Pattern[patternId] + else + patternId := UIA.Pattern.%(name := patternId)% + } catch + Throw ValueError("Failed to find pattern `"" name "`" among UIA.Pattern properties/values", -1) + DllCall("ole32\CLSIDFromString", "wstr", riid, "ptr", GUID:=Buffer(16)) + ComCall(15, this, "int", patternId, "ptr", GUID, "ptr*", &patternObject := 0) + return UIA.IUIAutomation%name%Pattern(patternObject) + } + + ; Retrieves the IUnknown interface of the specified control pattern on this UI Automation element. + ; This method gets the specified control pattern based on its availability at the time of the call. + ; For some forms of UI, this method will incur cross-process performance overhead. Applications can reduce overhead by caching control patterns and then retrieving them by using UIA.IUIAutomationElement,,GetCachedPattern. + GetPattern(patternId) { + local name, patternObject + try { + if IsInteger(patternId) + name := UIA.Pattern[patternId] + else + patternId := UIA.Pattern.%(name := StrReplace(patternId, "Pattern"))% + } catch + Throw ValueError("Failed to find pattern `"" name "`" among UIA.Pattern properties/values", -1) + ComCall(16, this, "int", patternId, "ptr*", &patternObject := 0) + return UIA.IUIAutomation%RegExReplace(name, "\d+$")%Pattern(patternObject) + } + + ; Retrieves from the cache the IUnknown interface of the specified control pattern of this UI Automation element. + GetCachedPattern(patternId) { + local name, patternObject + try { + if IsInteger(patternId) + name := UIA.Pattern[patternId] + else + patternId := UIA.Pattern.%(name := StrReplace(patternId, "Pattern"))% + } catch + Throw ValueError("Failed to find pattern `"" name "`" among UIA.Pattern properties/values", -1) + ComCall(17, this, "int", patternId, "ptr*", &patternObject := 0) + return UIA.IUIAutomation%RegExReplace(name, "\d+$")%Pattern(patternObject) + } + + ; Retrieves the identifier of the process that hosts the element. + ProcessId { + get { + local retVal + return (ComCall(20, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves the control type of the element. + ; Control types describe a known interaction model for UI Automation elements without relying on a localized control type or combination of complex logic rules. This property cannot change at run time unless the control supports the IUIAutomationMultipleViewPattern interface. An example is the Win32 ListView control, which can change from a data grid to a list, depending on the current view. + Type { + get { + local retVal + return (ComCall(21, this, "int*", &retVal := 0), retVal) + } + } + ControlType => this.Type + + ; Retrieves a localized description of the control type of the element. + LocalizedType { + get { + local retVal + return (ComCall(22, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + LocalizedControlType => this.LocalizedType + + ; Retrieves the name of the element. + Name { + get { + local retVal + return (ComCall(23, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + + ; Retrieves the accelerator key for the element. + AcceleratorKey { + get { + local retVal + return (ComCall(24, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + + ; Retrieves the access key character for the element. + ; An access key is a character in the text of a menu, menu item, or label of a control such as a button that activates the attached menu function. For example, the letter "O" is often used to invoke the Open file common dialog box from a File menu. Microsoft UI Automation elements that have the access key property set always implement the Invoke control pattern. + AccessKey { + get { + local retVal + return (ComCall(25, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + + ; Indicates whether the element has keyboard focus. + HasKeyboardFocus { + get { + local retVal + return (ComCall(26, this, "int*", &retVal := 0), retVal) + } + } + + ; Indicates whether the element can accept keyboard focus. + IsKeyboardFocusable { + get { + local retVal + return (ComCall(27, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves a cached value that indicates whether the element is enabled. + IsEnabled { + get { + local retVal + return (ComCall(28, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves the Microsoft UI Automation identifier of the element. + ; The identifier is unique among sibling elements in a container, and is the same in all instances of the application. + AutomationId { + get { + local retVal + return (ComCall(29, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + + ; Retrieves the class name of the element. + ; The value of this property is implementation-defined. The property is useful in testing environments. + ClassName { + get { + local retVal + return (ComCall(30, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + + ; Retrieves the help text for the element. This information is typically obtained from tooltips. + ; Caution Do not retrieve the CachedHelpText property from a control that is based on the SysListview32 class. Doing so could cause the system to become unstable and data to be lost. A client application can discover whether a control is based on SysListview32 by retrieving the CachedClassName or ClassName property from the control. + HelpText { + get { + local retVal + return (ComCall(31, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + + ; Retrieves the culture identifier for the element. + Culture { + get { + local retVal + return (ComCall(32, this, "int*", &retVal := 0), retVal) + } + } + + ; Indicates whether the element is a control element. + IsControlElement { + get { + local retVal + return (ComCall(33, this, "int*", &retVal := 0), retVal) + } + } + + ; Indicates whether the element is a content element. + ; A content element contains data that is presented to the user. Examples of content elements are the items in a list box or a button in a dialog box. Non-content elements, also called peripheral elements, are typically used to manipulate the content in a composite control; for example, the button on a drop-down control. + IsContentElement { + get { + local retVal + return (ComCall(34, this, "int*", &retVal := 0), retVal) + } + } + + ; Indicates whether the element contains a disguised password. + ; This property enables applications such as screen-readers to determine whether the text content of a control should be read aloud. + IsPassword { + get { + local retVal + return (ComCall(35, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves the window handle of the element. + NativeWindowHandle { + get { + local retVal + return (ComCall(36, this, "ptr*", &retVal := 0), retVal) + } + } + + ; Retrieves a description of the type of UI item represented by the element. + ; This property is used to obtain information about items in a list, tree view, or data grid. For example, an item in a file directory view might be a "Document File" or a "Folder". + ItemType { + get { + local retVal + return (ComCall(37, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + + ; Indicates whether the element is off-screen. + IsOffscreen { + get { + local retVal + return (ComCall(38, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves a value that indicates the orientation of the element. + ; This property is supported by controls such as scroll bars and sliders that can have either a vertical or a horizontal orientation. + Orientation { + get { + local retVal + return (ComCall(39, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves the name of the underlying UI framework. The name of the UI framework, such as "Win32", "WinForm", or "DirectUI". + FrameworkId { + get { + local retVal + return (ComCall(40, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + + ; Indicates whether the element is required to be filled out on a form. + IsRequiredForForm { + get { + local retVal + return (ComCall(41, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves the description of the status of an item in an element. + ; This property enables a client to ascertain whether an element is conveying status about an item. For example, an item associated with a contact in a messaging application might be "Busy" or "Connected". + ItemStatus { + get { + local retVal + return (ComCall(42, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + + ; Retrieves the coordinates of the rectangle that completely encloses the element, in screen coordinates. + BoundingRectangle => (ComCall(43, this, "ptr", retVal := UIA.NativeArray(0, 4, "int")), { l: retVal[0], t: retVal[1], r: retVal[2], b: retVal[3] }) + + ; This property maps to the Accessible Rich Internet Applications (ARIA) property. + + ; Retrieves the element that contains the text label for this element. + ; This property could be used to retrieve, for example, the static text label for a combo box. + LabeledBy { + get { + local retVal + return (ComCall(44, this, "ptr*", &retVal := 0), UIA.IUIAutomationElement(retVal)) + } + } + + ; Retrieves the Accessible Rich Internet Applications (ARIA) role of the element. + AriaRole { + get { + local retVal + return (ComCall(45, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + + ; Retrieves the ARIA properties of the element. + AriaProperties { + get { + local retVal + return (ComCall(46, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + + ; Indicates whether the element contains valid data for a form. + IsDataValidForForm { + get { + local retVal + return (ComCall(47, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves an array of elements for which this element serves as the controller. + ControllerFor { + get { + local retVal + return (ComCall(48, this, "ptr*", &retVal := 0), retVal?UIA.IUIAutomationElementArray(retVal).ToArray():[]) + } + } + + ; Retrieves an array of elements that describe this element. + DescribedBy { + get { + local retVal + return (ComCall(49, this, "ptr*", &retVal := 0), retVal?UIA.IUIAutomationElementArray(retVal).ToArray():[]) + } + } + + ; Retrieves an array of elements that indicates the reading order after the current element. + FlowsTo { + get { + local retVal + return (ComCall(50, this, "ptr*", &retVal := 0), retVal?UIA.IUIAutomationElementArray(retVal).ToArray():[]) + } + } + + ; Retrieves a description of the provider for this element. + ProviderDescription { + get { + local retVal + return (ComCall(51, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + + CachedRuntimeId => (this.DefineProp("CachedRuntimeId", {value:this.RuntimeId}), this.CachedRuntimeId) + + ; Retrieves the cached ID of the process that hosts the element. + CachedProcessId { + get { + local retVal + return (ComCall(52, this, "int*", &retVal := 0), this.DefineProp("CachedProcessId", {value: retVal}), this.CachedProcessId) + } + } + + ; Retrieves a cached value that indicates the control type of the element. + CachedType { + get { + local retVal + return (ComCall(53, this, "int*", &retVal := 0), this.DefineProp("CachedType", {value: retVal}), this.CachedType) + } + } + CachedControlType => this.CachedType + + ; Retrieves the cached localized description of the control type of the element. + CachedLocalizedType { + get { + local retVal + return (ComCall(54, this, "ptr*", &retVal := 0), this.DefineProp("CachedLocalizedType", {value: UIA.BSTR(retVal)}), this.CachedLocalizedType) + } + } + CachedLocalizedControlType => this.CachedLocalizedType + + ; Retrieves the cached name of the element. + CachedName { + get { + local retVal + return (ComCall(55, this, "ptr*", &retVal := 0), this.DefineProp("CachedName", {value: UIA.BSTR(retVal)}), this.CachedName) + } + } + + ; Retrieves the cached accelerator key for the element. + CachedAcceleratorKey { + get { + local retVal + return (ComCall(56, this, "ptr*", &retVal := 0), this.DefineProp("CachedAcceleratorKey", {value: UIA.BSTR(retVal)}), this.CachedAcceleratorKey) + } + } + + ; Retrieves the cached access key character for the element. + CachedAccessKey { + get { + local retVal + return (ComCall(57, this, "ptr*", &retVal := 0), this.DefineProp("CachedAccessKey", {value: UIA.BSTR(retVal)}), this.CachedAccessKey) + } + } + + ; A cached value that indicates whether the element has keyboard focus. + CachedHasKeyboardFocus { + get { + local retVal + return (ComCall(58, this, "int*", &retVal := 0), this.DefineProp("CachedHasKeyboardFocus", {value: retVal}), this.CachedHasKeyboardFocus) + } + } + + ; Retrieves a cached value that indicates whether the element can accept keyboard focus. + CachedIsKeyboardFocusable { + get { + local retVal + return (ComCall(59, this, "int*", &retVal := 0), this.DefineProp("CachedIsKeyboardFocusable", {value: retVal}), this.CachedIsKeyboardFocusable) + } + } + + ; Retrieves a cached value that indicates whether the element is enabled. + CachedIsEnabled { + get { + local retVal + return (ComCall(60, this, "int*", &retVal := 0), this.DefineProp("CachedIsEnabled", {value: retVal}), this.CachedIsEnabled) + } + } + + ; Retrieves the cached UI Automation identifier of the element. + CachedAutomationId { + get { + local retVal + return (ComCall(61, this, "ptr*", &retVal := 0), this.DefineProp("CachedAutomationId", {value: UIA.BSTR(retVal)}), this.CachedAutomationId) + } + } + + ; Retrieves the cached class name of the element. + CachedClassName { + get { + local retVal + return (ComCall(62, this, "ptr*", &retVal := 0), this.DefineProp("CachedClassName", {value: UIA.BSTR(retVal)}), this.CachedClassName) + } + } + + ; + CachedHelpText { + get { + local retVal + return (ComCall(63, this, "ptr*", &retVal := 0), this.DefineProp("CachedHelpText", {value: UIA.BSTR(retVal)}), this.CachedHelpText) + } + } + + ; Retrieves the cached help text for the element. + CachedCulture { + get { + local retVal + return (ComCall(64, this, "int*", &retVal := 0), this.DefineProp("CachedCulture", {value: retVal}), this.CachedCulture) + } + } + + ; Retrieves a cached value that indicates whether the element is a control element. + CachedIsControlElement { + get { + local retVal + return (ComCall(65, this, "int*", &retVal := 0), this.DefineProp("CachedIsControlElement", {value: retVal}), this.CachedIsControlElement) + } + } + + ; A cached value that indicates whether the element is a content element. + CachedIsContentElement { + get { + local retVal + return (ComCall(66, this, "int*", &retVal := 0), this.DefineProp("CachedIsContentElement", {value: retVal}), this.CachedIsContentElement) + } + } + + ; Retrieves a cached value that indicates whether the element contains a disguised password. + CachedIsPassword { + get { + local retVal + return (ComCall(67, this, "int*", &retVal := 0), this.DefineProp("CachedIsPassword", {value: retVal}), this.CachedIsPassword) + } + } + + ; Retrieves the cached window handle of the element. + CachedNativeWindowHandle { + get { + local retVal + return (ComCall(68, this, "ptr*", &retVal := 0), this.DefineProp("CachedNativeWindowHandle", {value: retVal}), this.CachedNativeWindowHandle) + } + } + + ; Retrieves a cached string that describes the type of item represented by the element. + CachedItemType { + get { + local retVal + return (ComCall(69, this, "ptr*", &retVal := 0), this.DefineProp("CachedItemType", {value: UIA.BSTR(retVal)}), this.CachedItemType) + } + } + + ; Retrieves a cached value that indicates whether the element is off-screen. + CachedIsOffscreen { + get { + local retVal + return (ComCall(70, this, "int*", &retVal := 0), this.DefineProp("CachedIsOffscreen", {value: retVal}), this.CachedIsOffscreen) + } + } + + ; Retrieves a cached value that indicates the orientation of the element. + CachedOrientation { + get { + local retVal + return (ComCall(71, this, "int*", &retVal := 0), this.DefineProp("CachedOrientation", {value: retVal}), this.CachedOrientation) + } + } + + ; Retrieves the cached name of the underlying UI framework associated with the element. + CachedFrameworkId { + get { + local retVal + return (ComCall(72, this, "ptr*", &retVal := 0), this.DefineProp("CachedFrameworkId", {value: UIA.BSTR(retVal)}), this.CachedFrameworkId) + } + } + + ; Retrieves a cached value that indicates whether the element is required to be filled out on a form. + CachedIsRequiredForForm { + get { + local retVal + return (ComCall(73, this, "int*", &retVal := 0), this.DefineProp("CachedIsRequiredForForm", {value: retVal}), this.CachedIsRequiredForForm) + } + } + + ; Retrieves a cached description of the status of an item within an element. + CachedItemStatus { + get { + local retVal + return (ComCall(74, this, "ptr*", &retVal := 0), this.DefineProp("CachedItemStatus", {value: UIA.BSTR(retVal)}), this.CachedItemStatus) + } + } + + ; Retrieves the cached coordinates of the rectangle that completely encloses the element. + CachedBoundingRectangle => (ComCall(75, this, "ptr", retVal := UIA.NativeArray(0, 4, "int")), this.DefineProp("CachedBoundingRectangle", {value: { l: retVal[0], t: retVal[1], r: retVal[2], b: retVal[3] }}), this.CachedBoundingRectangle) + + ; Retrieves the cached element that contains the text label for this element. + CachedLabeledBy { + get { + local retVal + return (ComCall(76, this, "ptr*", &retVal := 0), this.DefineProp("CachedLabeledBy", {value: UIA.IUIAutomationElement(retVal)}), this.CachedLabeledBy) + } + } + + ; Retrieves the cached ARIA role of the element. + CachedAriaRole { + get { + local retVal + return (ComCall(77, this, "ptr*", &retVal := 0), this.DefineProp("CachedAriaRole", {value: UIA.BSTR(retVal)}), this.CachedAriaRole) + } + } + + ; Retrieves the cached ARIA properties of the element. + CachedAriaProperties { + get { + local retVal + return (ComCall(78, this, "ptr*", &retVal := 0), this.DefineProp("CachedAriaProperties", {value: UIA.BSTR(retVal)}), this.CachedAriaProperties) + } + } + + ; Retrieves a cached value that indicates whether the element contains valid data for the form. + CachedIsDataValidForForm { + get { + local retVal + return (ComCall(79, this, "int*", &retVal := 0), this.DefineProp("CachedIsDataValidForForm", {value: retVal}), this.CachedIsDataValidForForm) + } + } + + ; Retrieves a cached array of UI Automation elements for which this element serves as the controller. + CachedControllerFor { + get { + local retVal + return (ComCall(80, this, "ptr*", &retVal := 0), this.DefineProp("CachedControllerFor", {value: retVal?UIA.IUIAutomationElementArray(retVal).ToArray():[]}), this.CachedControllerFor) + } + } + + ; Retrieves a cached array of elements that describe this element. + CachedDescribedBy { + get { + local retVal + return (ComCall(81, this, "ptr*", &retVal := 0), this.DefineProp("CachedDescribedBy", {value: retVal?UIA.IUIAutomationElementArray(retVal).ToArray():[]}), this.CachedDescribedBy) + } + } + + ; Retrieves a cached array of elements that indicate the reading order after the current element. + CachedFlowsTo { + get { + local retVal + return (ComCall(82, this, "ptr*", &retVal := 0), this.DefineProp("CachedFlowsTo", {value: retVal?UIA.IUIAutomationElementArray(retVal).ToArray():[]}), this.CachedFlowsTo) + } + } + + ; Retrieves a cached description of the provider for this element. + CachedProviderDescription { + get { + local retVal + return (ComCall(83, this, "ptr*", &retVal := 0), this.DefineProp("CachedProviderDescription", {value: UIA.BSTR(retVal)}), this.CachedProviderDescription) + } + } + + ; Retrieves a point on the element that can be clicked. + ; A client application can use this method to simulate clicking the left or right mouse button. For example, to simulate clicking the right mouse button to display the context menu for a control, + ; • Call the GetClickablePoint method to find a clickable point on the control. + ; • Call the SendInput function to send a right-mouse-down, right-mouse-up sequence. + GetClickablePoint() { + if (ComCall(84, this, "ptr", clickable := Buffer(8), "int*", &gotClickable := 0), gotClickable) + return { x: NumGet(clickable, 0, 'int'), y: NumGet(clickable, 4, 'int') } + throw TargetError('The element has no clickable point') + } + + ;; UIA.IUIAutomationElement2 + OptimizeForVisualContent { + get { + local retVal + return (ComCall(85, this, "int*", &retVal := 0), retVal) + } + } + CachedOptimizeForVisualContent { + get { + local retVal + return (ComCall(86, this, "int*", &retVal := 0), retVal) + } + } + LiveSetting { + get { + local retVal + return (ComCall(87, this, "int*", &retVal := 0), retVal) + } + } + CachedLiveSetting { + get { + local retVal + return (ComCall(88, this, "int*", &retVal := 0), retVal) + } + } + FlowsFrom { + get { + local retVal + return (ComCall(89, this, "ptr*", &retVal := 0), retVal?UIA.IUIAutomationElementArray(retVal).ToArray():[]) + } + } + CachedFlowsFrom { + get { + local retVal + return (ComCall(90, this, "ptr*", &retVal := 0), retVal?UIA.IUIAutomationElementArray(retVal).ToArray():[]) + } + } + + ;; UIA.IUIAutomationElement3 + ShowContextMenu() => ComCall(91, this) + IsPeripheral { + get { + local retVal + return (ComCall(92, this, "int*", &retVal := 0), retVal) + } + } + CachedIsPeripheral { + get { + local retVal + return (ComCall(93, this, "int*", &retVal := 0), retVal) + } + } + + ;; UIA.IUIAutomationElement4 + PositionInSet { + get { + local retVal + return (ComCall(94, this, "int*", &retVal := 0), retVal) + } + } + SizeOfSet { + get { + local retVal + return (ComCall(95, this, "int*", &retVal := 0), retVal) + } + } + Level { + get { + local retVal + return (ComCall(96, this, "int*", &retVal := 0), retVal) + } + } + AnnotationTypes { + get { + local retVal + return (ComCall(97, this, "ptr*", &retVal := 0), ComValue(0x2003, retVal)) + } + } + AnnotationObjects { + get { + local retVal + return (ComCall(98, this, "ptr*", &retVal := 0), retVal?UIA.IUIAutomationElementArray(retVal).ToArray():[]) + } + } + CachedPositionInSet { + get { + local retVal + return (ComCall(99, this, "int*", &retVal := 0), retVal) + } + } + CachedSizeOfSet { + get { + local retVal + return (ComCall(100, this, "int*", &retVal := 0), retVal) + } + } + CachedLevel { + get { + local retVal + return (ComCall(101, this, "int*", &retVal := 0), retVal) + } + } + CachedAnnotationTypes { + get { + local retVal + return (ComCall(102, this, "ptr*", &retVal := 0), ComValue(0x2003, retVal)) + } + } + CachedAnnotationObjects { + get { + local retVal + return (ComCall(103, this, "ptr*", &retVal := 0), retVal?UIA.IUIAutomationElementArray(retVal).ToArray():[]) + } + } + + ;; UIA.IUIAutomationElement5 + LandmarkType { + get { + local retVal + return (ComCall(104, this, "int*", &retVal := 0), retVal) + } + } + LocalizedLandmarkType { + get { + local retVal + return (ComCall(105, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + CachedLandmarkType { + get { + local retVal + return (ComCall(106, this, "int*", &retVal := 0), retVal) + } + } + CachedLocalizedLandmarkType { + get { + local retVal + return (ComCall(107, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + + ;; UIA.IUIAutomationElement6 + FullDescription { + get { + local retVal + return (ComCall(108, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + CachedFullDescription { + get { + local retVal + return (ComCall(109, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + + ;; UIA.IUIAutomationElement7 + ; FindFirst with additional parameters + ; This gets called when FindFirst or FindElement is used with traversalOptions or startingElement arguments + FindFirstWithOptions(condition, traversalOptions:=0, root:=0, scope:=4) { + local found + if (ComCall(110, this, "int", UIA.TypeValidation.TreeScope(scope), "ptr", (Type(condition) ~= "i)Condition|ComValue") ? condition : UIA.__CreateRawCondition(condition), "int", UIA.TypeValidation.TreeTraversalOptions(traversalOptions), "ptr", root, "ptr*", &found := 0), found) + return UIA.IUIAutomationElement(found) + throw TargetError("An element matching the condition was not found", -1) + } + ; FindAll with additional parameters + ; This gets called when FindAll or FindElements is used with traversalOptions or startingElement arguments + FindAllWithOptions(condition, traversalOptions:=0, root:=0, scope:=4) { + local found + if (ComCall(111, this, "int", UIA.TypeValidation.TreeScope(scope), "ptr", (Type(condition) ~= "i)Condition|ComValue") ? condition : UIA.__CreateRawCondition(condition), "int", UIA.TypeValidation.TreeTraversalOptions(traversalOptions), "ptr", root, "ptr*", &found := 0), found) + return UIA.IUIAutomationElementArray(found).ToArray() + return [] + } + + ; TreeScope, IUIAutomationCondition, IUIAutomationCacheRequest, TreeTraversalOptions, UIA.IUIAutomationElement + FindFirstWithOptionsBuildCache(cacheRequest, condition, traversalOptions:=0, root:=0, scope := 4) { + local found + if (ComCall(112, this, "int", UIA.TypeValidation.TreeScope(scope), "ptr", InStr(Type(condition), "Condition") ? condition : UIA.__CreateRawCondition(condition), "ptr", UIA.TypeValidation.CacheRequest(cacheRequest), "int", traversalOptions, "ptr", root, "ptr*", &found := 0), found) + return UIA.IUIAutomationElement(found) + throw TargetError("An element matching the condition was not found", -1) + } + FindAllWithOptionsBuildCache(cacheRequest, condition, traversalOptions:=0, root:=0, scope := 4) { + local found + if (ComCall(113, this, "int", UIA.TypeValidation.TreeScope(scope), "ptr", InStr(Type(condition), "Condition") ? condition : UIA.__CreateRawCondition(condition), "ptr", UIA.TypeValidation.CacheRequest(cacheRequest), "int", traversalOptions, "ptr", root, "ptr*", &found := 0), found) + return UIA.IUIAutomationElementArray(found).ToArray() + return [] + } + GetMetadataValue(targetId, metadataId) { + local returnVal + return (ComCall(114, this, "int", targetId, "int", metadataId, "ptr", returnVal := UIA.ComVar()), returnVal[]) + } + + ;; UIA.IUIAutomationElement8 + HeadingLevel { + get { + local retVal + return (ComCall(115, this, "int*", &retVal := 0), retVal) + } + } + CachedHeadingLevel { + get { + local retVal + return (ComCall(116, this, "int*", &retVal := 0), retVal) + } + } + + ;; UIA.IUIAutomationElement9 + IsDialog { + get { + local retVal + return (ComCall(117, this, "int*", &retVal := 0), retVal) + } + } + CachedIsDialog { + get { + local retVal + return (ComCall(118, this, "int*", &retVal := 0), retVal) + } + } + + ;; Some aliases for properties from GetPropertyValue + /* + ClickablePoint => this.GetPropertyValue(UIA.Property.ClickablePoint) + IsDockPatternAvailable => this.GetPropertyValue(UIA.Property.IsDockPatternAvailable) + IsExpandCollapsePatternAvailable => this.GetPropertyValue(UIA.Property.IsExpandCollapsePatternAvailable) + IsGridItemPatternAvailable => this.GetPropertyValue(UIA.Property.IsGridItemPatternAvailable) + IsGridPatternAvailable => this.GetPropertyValue(UIA.Property.IsGridPatternAvailable) + IsInvokePatternAvailable => this.GetPropertyValue(UIA.Property.IsInvokePatternAvailable) + IsMultipleViewPatternAvailable => this.GetPropertyValue(UIA.Property.IsMultipleViewPatternAvailable) + IsRangeValuePatternAvailable => this.GetPropertyValue(UIA.Property.IsRangeValuePatternAvailable) + IsScrollPatternAvailable => this.GetPropertyValue(UIA.Property.IsScrollPatternAvailable) + IsScrollItemPatternAvailable => this.GetPropertyValue(UIA.Property.IsScrollItemPatternAvailable) + IsSelectionItemPatternAvailable => this.GetPropertyValue(UIA.Property.IsSelectionItemPatternAvailable) + IsSelectionPatternAvailable => this.GetPropertyValue(UIA.Property.IsSelectionPatternAvailable) + IsTablePatternAvailable => this.GetPropertyValue(UIA.Property.IsTablePatternAvailable) + IsTableItemPatternAvailable => this.GetPropertyValue(UIA.Property.IsTableItemPatternAvailable) + IsTextPatternAvailable => this.GetPropertyValue(UIA.Property.IsTextPatternAvailable) + IsTogglePatternAvailable => this.GetPropertyValue(UIA.Property.IsTogglePatternAvailable) + IsTransformPatternAvailable => this.GetPropertyValue(UIA.Property.IsTransformPatternAvailable) + IsValuePatternAvailable => this.GetPropertyValue(UIA.Property.IsValuePatternAvailable) + IsWindowPatternAvailable => this.GetPropertyValue(UIA.Property.IsWindowPatternAvailable) + ValueIsReadOnly => this.GetPropertyValue(UIA.Property.ValueIsReadOnly) + RangeValueValue => this.GetPropertyValue(UIA.Property.RangeValueValue) + RangeValueIsReadOnly => this.GetPropertyValue(UIA.Property.RangeValueIsReadOnly) + RangeValueMinimum => this.GetPropertyValue(UIA.Property.RangeValueMinimum) + RangeValueMaximum => this.GetPropertyValue(UIA.Property.RangeValueMaximum) + RangeValueLargeChange => this.GetPropertyValue(UIA.Property.RangeValueLargeChange) + RangeValueSmallChange => this.GetPropertyValue(UIA.Property.RangeValueSmallChange) + ScrollHorizontalScrollPercent => this.GetPropertyValue(UIA.Property.ScrollHorizontalScrollPercent) + ScrollHorizontalViewSize => this.GetPropertyValue(UIA.Property.ScrollHorizontalViewSize) + ScrollVerticalScrollPercent => this.GetPropertyValue(UIA.Property.ScrollVerticalScrollPercent) + ScrollVerticalViewSize => this.GetPropertyValue(UIA.Property.ScrollVerticalViewSize) + ScrollHorizontallyScrollable => this.GetPropertyValue(UIA.Property.ScrollHorizontallyScrollable) + ScrollVerticallyScrollable => this.GetPropertyValue(UIA.Property.ScrollVerticallyScrollable) + SelectionSelection => this.GetPropertyValue(UIA.Property.SelectionSelection) + SelectionCanSelectMultiple => this.GetPropertyValue(UIA.Property.SelectionCanSelectMultiple) + SelectionIsSelectionRequired => this.GetPropertyValue(UIA.Property.SelectionIsSelectionRequired) + GridRowCount => this.GetPropertyValue(UIA.Property.GridRowCount) + GridColumnCount => this.GetPropertyValue(UIA.Property.GridColumnCount) + GridItemRow => this.GetPropertyValue(UIA.Property.GridItemRow) + GridItemColumn => this.GetPropertyValue(UIA.Property.GridItemColumn) + GridItemRowSpan => this.GetPropertyValue(UIA.Property.GridItemRowSpan) + GridItemColumnSpan => this.GetPropertyValue(UIA.Property.GridItemColumnSpan) + GridItemContainingGrid => this.GetPropertyValue(UIA.Property.GridItemContainingGrid) + DockDockPosition => this.GetPropertyValue(UIA.Property.DockDockPosition) + ExpandCollapseExpandCollapseState => this.GetPropertyValue(UIA.Property.ExpandCollapseExpandCollapseState) + MultipleViewCurrentView => this.GetPropertyValue(UIA.Property.MultipleViewCurrentView) + MultipleViewSupportedViews => this.GetPropertyValue(UIA.Property.MultipleViewSupportedViews) + WindowCanMaximize => this.GetPropertyValue(UIA.Property.WindowCanMaximize) + WindowCanMinimize => this.GetPropertyValue(UIA.Property.WindowCanMinimize) + WindowWindowVisualState => this.GetPropertyValue(UIA.Property.WindowWindowVisualState) + WindowWindowInteractionState => this.GetPropertyValue(UIA.Property.WindowWindowInteractionState) + WindowIsModal => this.GetPropertyValue(UIA.Property.WindowIsModal) + WindowIsTopmost => this.GetPropertyValue(UIA.Property.WindowIsTopmost) + SelectionItemIsSelected => this.GetPropertyValue(UIA.Property.SelectionItemIsSelected) + SelectionItemSelectionContainer => this.GetPropertyValue(UIA.Property.SelectionItemSelectionContainer) + TableRowHeaders => this.GetPropertyValue(UIA.Property.TableRowHeaders) + TableColumnHeaders => this.GetPropertyValue(UIA.Property.TableColumnHeaders) + TableRowOrColumnMajor => this.GetPropertyValue(UIA.Property.TableRowOrColumnMajor) + TableItemRowHeaderItems => this.GetPropertyValue(UIA.Property.TableItemRowHeaderItems) + TableItemColumnHeaderItems => this.GetPropertyValue(UIA.Property.TableItemColumnHeaderItems) + ToggleToggleState => this.GetPropertyValue(UIA.Property.ToggleToggleState) + TransformCanMove => this.GetPropertyValue(UIA.Property.TransformCanMove) + TransformCanResize => this.GetPropertyValue(UIA.Property.TransformCanResize) + TransformCanRotate => this.GetPropertyValue(UIA.Property.TransformCanRotate) + IsLegacyIAccessiblePatternAvailable => this.GetPropertyValue(UIA.Property.IsLegacyIAccessiblePatternAvailable) + LegacyIAccessibleChildId => this.GetPropertyValue(UIA.Property.LegacyIAccessibleChildId) + LegacyIAccessibleName => this.GetPropertyValue(UIA.Property.LegacyIAccessibleName) + LegacyIAccessibleValue => this.GetPropertyValue(UIA.Property.LegacyIAccessibleValue) + LegacyIAccessibleDescription => this.GetPropertyValue(UIA.Property.LegacyIAccessibleDescription) + LegacyIAccessibleRole => this.GetPropertyValue(UIA.Property.LegacyIAccessibleRole) + LegacyIAccessibleState => this.GetPropertyValue(UIA.Property.LegacyIAccessibleState) + LegacyIAccessibleHelp => this.GetPropertyValue(UIA.Property.LegacyIAccessibleHelp) + LegacyIAccessibleKeyboardShortcut => this.GetPropertyValue(UIA.Property.LegacyIAccessibleKeyboardShortcut) + LegacyIAccessibleSelection => this.GetPropertyValue(UIA.Property.LegacyIAccessibleSelection) + LegacyIAccessibleDefaultAction => this.GetPropertyValue(UIA.Property.LegacyIAccessibleDefaultAction) + IsItemContainerPatternAvailable => this.GetPropertyValue(UIA.Property.IsItemContainerPatternAvailable) + IsVirtualizedItemPatternAvailable => this.GetPropertyValue(UIA.Property.IsVirtualizedItemPatternAvailable) + IsSynchronizedInputPatternAvailable => this.GetPropertyValue(UIA.Property.IsSynchronizedInputPatternAvailable) + IsObjectModelPatternAvailable => this.GetPropertyValue(UIA.Property.IsObjectModelPatternAvailable) + AnnotationAnnotationTypeId => this.GetPropertyValue(UIA.Property.AnnotationAnnotationTypeId) + AnnotationAnnotationTypeName => this.GetPropertyValue(UIA.Property.AnnotationAnnotationTypeName) + AnnotationAuthor => this.GetPropertyValue(UIA.Property.AnnotationAuthor) + AnnotationDateTime => this.GetPropertyValue(UIA.Property.AnnotationDateTime) + AnnotationTarget => this.GetPropertyValue(UIA.Property.AnnotationTarget) + IsAnnotationPatternAvailable => this.GetPropertyValue(UIA.Property.IsAnnotationPatternAvailable) + IsTextPattern2Available => this.GetPropertyValue(UIA.Property.IsTextPattern2Available) + StylesStyleId => this.GetPropertyValue(UIA.Property.StylesStyleId) + StylesStyleName => this.GetPropertyValue(UIA.Property.StylesStyleName) + StylesFillColor => this.GetPropertyValue(UIA.Property.StylesFillColor) + StylesFillPatternStyle => this.GetPropertyValue(UIA.Property.StylesFillPatternStyle) + StylesShape => this.GetPropertyValue(UIA.Property.StylesShape) + StylesFillPatternColor => this.GetPropertyValue(UIA.Property.StylesFillPatternColor) + StylesExtendedProperties => this.GetPropertyValue(UIA.Property.StylesExtendedProperties) + IsStylesPatternAvailable => this.GetPropertyValue(UIA.Property.IsStylesPatternAvailable) + IsSpreadsheetPatternAvailable => this.GetPropertyValue(UIA.Property.IsSpreadsheetPatternAvailable) + SpreadsheetItemFormula => this.GetPropertyValue(UIA.Property.SpreadsheetItemFormula) + SpreadsheetItemAnnotationObjects => this.GetPropertyValue(UIA.Property.SpreadsheetItemAnnotationObjects) + SpreadsheetItemAnnotationTypes => this.GetPropertyValue(UIA.Property.SpreadsheetItemAnnotationTypes) + IsSpreadsheetItemPatternAvailable => this.GetPropertyValue(UIA.Property.IsSpreadsheetItemPatternAvailable) + Transform2CanZoom => this.GetPropertyValue(UIA.Property.Transform2CanZoom) + IsTransformPattern2Available => this.GetPropertyValue(UIA.Property.IsTransformPattern2Available) + IsTextChildPatternAvailable => this.GetPropertyValue(UIA.Property.IsTextChildPatternAvailable) + IsDragPatternAvailable => this.GetPropertyValue(UIA.Property.IsDragPatternAvailable) + DragIsGrabbed => this.GetPropertyValue(UIA.Property.DragIsGrabbed) + DragDropEffect => this.GetPropertyValue(UIA.Property.DragDropEffect) + DragDropEffects => this.GetPropertyValue(UIA.Property.DragDropEffects) + IsDropTargetPatternAvailable => this.GetPropertyValue(UIA.Property.IsDropTargetPatternAvailable) + DropTargetDropTargetEffect => this.GetPropertyValue(UIA.Property.DropTargetDropTargetEffect) + DropTargetDropTargetEffects => this.GetPropertyValue(UIA.Property.DropTargetDropTargetEffects) + DragGrabbedItems => this.GetPropertyValue(UIA.Property.DragGrabbedItems) + Transform2ZoomLevel => this.GetPropertyValue(UIA.Property.Transform2ZoomLevel) + Transform2ZoomMinimum => this.GetPropertyValue(UIA.Property.Transform2ZoomMinimum) + Transform2ZoomMaximum => this.GetPropertyValue(UIA.Property.Transform2ZoomMaximum) + IsTextEditPatternAvailable => this.GetPropertyValue(UIA.Property.IsTextEditPatternAvailable) + IsCustomNavigationPatternAvailable => this.GetPropertyValue(UIA.Property.IsCustomNavigationPatternAvailable) + FillColor => this.GetPropertyValue(UIA.Property.FillColor) + OutlineColor => this.GetPropertyValue(UIA.Property.OutlineColor) + FillType => this.GetPropertyValue(UIA.Property.FillType) + VisualEffects => this.GetPropertyValue(UIA.Property.VisualEffects) + OutlineThickness => this.GetPropertyValue(UIA.Property.OutlineThickness) + CenterPoint => this.GetPropertyValue(UIA.Property.CenterPoint) + Rotation => this.GetPropertyValue(UIA.Property.Rotation) + Size => this.GetPropertyValue(UIA.Property.Size) + IsSelectionPattern2Available => this.GetPropertyValue(UIA.Property.IsSelectionPattern2Available) + Selection2FirstSelectedItem => this.GetPropertyValue(UIA.Property.Selection2FirstSelectedItem) + Selection2LastSelectedItem => this.GetPropertyValue(UIA.Property.Selection2LastSelectedItem) + Selection2CurrentSelectedItem => this.GetPropertyValue(UIA.Property.Selection2CurrentSelectedItem) + Selection2ItemCount => this.GetPropertyValue(UIA.Property.Selection2ItemCount) + */ +} + +class IUIAutomationArray extends UIA.IUIAutomationBase { + /** + * Element[i] returns the i-th element. + * Element[-i] returns the i-th last element: Element[-1] will return the last element. + */ + __Item[index] { + get { + try { + if index>0 + return this.GetElement(index-1) + else + return this.GetElement(this.Length+index) + } catch + throw IndexError("Index " index " out of bounds", -2) + } + } + __Enum(varCount) { + local maxLen := this.Length-1, i := 0 + EnumElements(&element) { + if i > maxLen + return false + element := this.GetElement(i++) + return true + } + EnumIndexAndElements(&index, &element) { + if i > maxLen + return false + element := this.GetElement(i++) + index := i + return true + } + return (varCount = 1) ? EnumElements : EnumIndexAndElements + } + ; Converts an IUIAutomationArray into AHK array + ToArray() { + local ret := [] + Loop this.Length + ret.Push(this.GetElement(A_index-1)) + return ret + } + ToReversedArray() { + local ret := [], len := this.Length + Loop len + ret.Push(this.GetElement(len-A_Index)) + return ret + } +} + +class IUIAutomationElementArray extends UIA.IUIAutomationArray { + ; Retrieves the number of elements in the collection. + Length { + get { + local length + return (ComCall(3, this, "int*", &length := 0), length) + } + } + + ; Retrieves a Microsoft UI Automation element from the collection. + ; Index is 0-based + GetElement(index) { + local element + return (ComCall(4, this, "int", index, "ptr*", &element := 0), UIA.IUIAutomationElement(element)) + } +} + +/* + Exposes properties and methods that UI Automation client applications use to view and navigate the UI Automation elements on the desktop. + Microsoft documentation: https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nn-uiautomationclient-iuiautomationtreewalker +*/ +class IUIAutomationTreeWalker extends UIA.IUIAutomationBase { + ; The structure of the Microsoft UI Automation tree changes as the visible UI elements on the desktop change. + ; An element can have additional child elements that do not match the current view condition and thus are not returned when navigating the element tree. + + ; Retrieves the parent element of the specified UI Automation element. + GetParentElement(element) { + local out + if (ComCall(3, this, "ptr", UIA.TypeValidation.Element(element), "ptr*", &out := 0), out) + return UIA.IUIAutomationElement(out) + throw UnsetError("No element found with GetParentElement", -1) + } + TryGetParentElement(element) { + local out + try if (ComCall(3, this, "ptr", element, "ptr*", &out := 0), out) + return UIA.IUIAutomationElement(out) + return 0 + } + + ; Retrieves the first child element of the specified UI Automation element. + GetFirstChildElement(element) { + local out + if (ComCall(4, this, "ptr", UIA.TypeValidation.Element(element), "ptr*", &out := 0), out) + return UIA.IUIAutomationElement(out) + throw UnsetError("No element found with GetFirstChildElement", -1) + } + TryGetFirstChildElement(element) { + local out + try if (ComCall(4, this, "ptr", element, "ptr*", &out := 0), out) + return UIA.IUIAutomationElement(out) + return 0 + } + + ; Retrieves the last child element of the specified UI Automation element. + GetLastChildElement(element) { + local out + if (ComCall(5, this, "ptr", UIA.TypeValidation.Element(element), "ptr*", &out := 0), out) + return UIA.IUIAutomationElement(out) + throw UnsetError("No element found with GetLastChildElement", -1) + } + TryGetLastChildElement(element) { + local out + try if (ComCall(5, this, "ptr", element, "ptr*", &out := 0), out) + return UIA.IUIAutomationElement(out) + return 0 + } + + ; Retrieves the next sibling element of the specified UI Automation element, and caches properties and control patterns. + GetNextSiblingElement(element) { + local out + if (ComCall(6, this, "ptr", UIA.TypeValidation.Element(element), "ptr*", &out := 0), out) + return UIA.IUIAutomationElement(out) + throw UnsetError("No element found with GetNextSiblingElement", -1) + } + TryGetNextSiblingElement(element) { + local out + try if (ComCall(6, this, "ptr", element, "ptr*", &out := 0), out) + return UIA.IUIAutomationElement(out) + return 0 + } + + ; Retrieves the previous sibling element of the specified UI Automation element, and caches properties and control patterns. + GetPreviousSiblingElement(element) { + local out + if (ComCall(7, this, "ptr", UIA.TypeValidation.Element(element), "ptr*", &out := 0), out) + return UIA.IUIAutomationElement(out) + throw UnsetError("No element found with GetPreviousSiblingElement", -1) + } + TryGetPreviousSiblingElement(element) { + local out + try if (ComCall(7, this, "ptr", element, "ptr*", &out := 0), out) + return UIA.IUIAutomationElement(out) + return 0 + } + + ; Retrieves the ancestor element nearest to the specified Microsoft UI Automation element in the tree view. + ; The element is normalized by navigating up the ancestor chain in the tree until an element that satisfies the view condition (specified by a previous call to IUIAutomationTreeWalker,,Condition) is reached. If the root element is reached, the root element is returned, even if it does not satisfy the view condition. + ; This method is useful for applications that obtain references to UI Automation elements by hit-testing. The application might want to work only with specific types of elements, and can use IUIAutomationTreeWalker,,Normalize to make sure that no matter what element is initially retrieved (for example, when a scroll bar gets the input focus), only the element of interest (such as a content element) is ultimately retrieved. + NormalizeElement(element) { + local out + if (ComCall(8, this, "ptr", UIA.TypeValidation.Element(element), "ptr*", &out := 0), out) + return UIA.IUIAutomationElement(out) + throw UnsetError("No element found with NormalizeElement", -1) + } + TryNormalizeElement(element) { + local out + try if (ComCall(8, this, "ptr", element, "ptr*", &out := 0), out) + return UIA.IUIAutomationElement(out) + return 0 + } + + ; Retrieves the parent element of the specified UI Automation element, and caches properties and control patterns. + GetParentElementBuildCache(cacheRequest, element) { + local out + if (ComCall(9, this, "ptr", UIA.TypeValidation.Element(element), "ptr", UIA.TypeValidation.CacheRequest(cacheRequest), "ptr*", &out := 0), out) + return UIA.IUIAutomationElement(out) + throw UnsetError("No element found with GetParentElementBuildCache", -1) + } + TryGetParentElementBuildCache(cacheRequest, element) { + local out + try if (ComCall(9, this, "ptr", element, "ptr", cacheRequest, "ptr*", &out := 0), out) + return UIA.IUIAutomationElement(out) + return 0 + } + + ; Retrieves the first child element of the specified UI Automation element, and caches properties and control patterns. + GetFirstChildElementBuildCache(cacheRequest, element) { + local out + if (ComCall(10, this, "ptr", UIA.TypeValidation.Element(element), "ptr", UIA.TypeValidation.CacheRequest(cacheRequest), "ptr*", &out := 0), out) + return UIA.IUIAutomationElement(out) + throw UnsetError("No element found with GetFirstChildElementBuildCache", -1) + } + TryGetFirstChildElementBuildCache(cacheRequest, element) { + local out + try if (ComCall(10, this, "ptr", element, "ptr", cacheRequest, "ptr*", &out := 0), out) + return UIA.IUIAutomationElement(out) + return 0 + } + + ; Retrieves the last child element of the specified UI Automation element, and caches properties and control patterns. + GetLastChildElementBuildCache(cacheRequest, element) { + local out + if (ComCall(11, this, "ptr", UIA.TypeValidation.Element(element), "ptr", UIA.TypeValidation.CacheRequest(cacheRequest), "ptr*", &out := 0), out) + return UIA.IUIAutomationElement(out) + throw UnsetError("No element found with GetLastChildElementBuildCache", -1) + } + TryGetLastChildElementBuildCache(cacheRequest, element) { + local out + try if (ComCall(11, this, "ptr", element, "ptr", cacheRequest, "ptr*", &out := 0), out) + return UIA.IUIAutomationElement(out) + return 0 + } + + ; Retrieves the next sibling element of the specified UI Automation element, and caches properties and control patterns. + GetNextSiblingElementBuildCache(cacheRequest, element) { + local out + if (ComCall(12, this, "ptr", UIA.TypeValidation.Element(element), "ptr", UIA.TypeValidation.CacheRequest(cacheRequest), "ptr*", &out := 0), out) + return UIA.IUIAutomationElement(out) + throw UnsetError("No element found with GetNextSiblingElementBuildCache", -1) + } + TryGetNextSiblingElementBuildCache(cacheRequest, element) { + local out + try if (ComCall(12, this, "ptr", element, "ptr", cacheRequest, "ptr*", &out := 0), out) + return UIA.IUIAutomationElement(out) + return 0 + } + + ; Retrieves the previous sibling element of the specified UI Automation element, and caches properties and control patterns. + GetPreviousSiblingElementBuildCache(cacheRequest, element) { + local out + if (ComCall(13, this, "ptr", UIA.TypeValidation.Element(element), "ptr", UIA.TypeValidation.CacheRequest(cacheRequest), "ptr*", &out := 0), out) + return UIA.IUIAutomationElement(out) + throw UnsetError("No element found with GetPreviousSiblingElementBuildCache", -1) + } + TryGetPreviousSiblingElementBuildCache(cacheRequest, element) { + local out + try if (ComCall(13, this, "ptr", element, "ptr", cacheRequest, "ptr*", &out := 0), out) + return UIA.IUIAutomationElement(out) + return 0 + } + + ; Retrieves the ancestor element nearest to the specified Microsoft UI Automation element in the tree view, prefetches the requested properties and control patterns, and stores the prefetched items in the cache. + NormalizeElementBuildCache(cacheRequest, element) { + local out + if (ComCall(14, this, "ptr", UIA.TypeValidation.Element(element), "ptr", UIA.TypeValidation.CacheRequest(cacheRequest), "ptr*", &out := 0), out) + return UIA.IUIAutomationElement(out) + throw UnsetError("No element found with NormalizeElementBuildCache", -1) + } + TryNormalizeElementBuildCache(cacheRequest, element) { + local out + try if (ComCall(14, this, "ptr", element, "ptr", cacheRequest, "ptr*", &out := 0), out) + return UIA.IUIAutomationElement(out) + return 0 + } + + ; Retrieves the condition that defines the view of the UI Automation tree. This property is read-only. + ; The condition that defines the view. This is the interface that was passed to CreateTreeWalker. + Condition { + get { + local condition + return (ComCall(15, this, "ptr*", &condition := 0), UIA.IUIAutomationCondition(condition)) + } + } +} + +/* + Represents a condition based on a property value that is used to find UI Automation elements. + Microsoft documentation: https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nn-uiautomationclient-iuiautomationpropertycondition +*/ +class IUIAutomationPropertyCondition extends UIA.IUIAutomationCondition { + static __IID := "{99ebf2cb-5578-4267-9ad4-afd6ea77e94b}" + PropertyId { + get { + local propertyId + return (ComCall(3, this, "int*", &propertyId := 0), propertyId) + } + } + PropertyValue { + get { + local propertyValue + return (ComCall(4, this, "ptr", propertyValue := UIA.ComVar()), propertyValue[]) + } + } + PropertyConditionFlags { + get { + local flags + return (ComCall(5, this, "int*", &flags := 0), flags) + } + } +} + +/* + Exposes properties and methods that Microsoft UI Automation client applications can use to retrieve information about an AND-based property condition. + Microsoft documentation: https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nn-uiautomationclient-iuiautomationandcondition +*/ +class IUIAutomationAndCondition extends UIA.IUIAutomationCondition { + static __IID := "{a7d0af36-b912-45fe-9855-091ddc174aec}" + ChildCount { + get { + local childCount + return (ComCall(3, this, "int*", &childCount := 0), childCount) + } + } + GetChildrenAsNativeArray() { + local childArrayCount + return (ComCall(4, this, "ptr*", &childArray := 0, "int*", &childArrayCount := 0), UIA.NativeArray(childArray, childArrayCount)) + } + GetChildren() { + local childArray + return (ComCall(5, this, "ptr*", &childArray := 0), UIA.IUIAutomationCondition.__SafeArrayToConditionArray(childArray)) + } +} + +/* + Represents a condition made up of multiple conditions, at least one of which must be true. + Microsoft documentation: https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nn-uiautomationclient-iuiautomationorcondition +*/ +class IUIAutomationOrCondition extends UIA.IUIAutomationAndCondition { + static __IID := "{8753f032-3db1-47b5-a1fc-6e34a266c712}" + ChildCount { + get { + local childCount + return (ComCall(3, this, "int*", &childCount := 0), childCount) + } + } + GetChildrenAsNativeArray() { + local childArrayCount + return (ComCall(4, this, "ptr*", &childArray := 0, "int*", &childArrayCount := 0), UIA.NativeArray(childArray, childArrayCount)) + } + GetChildren() { + local childArray + return (ComCall(5, this, "ptr*", &childArray := 0), UIA.IUIAutomationCondition.__SafeArrayToConditionArray(childArray)) + } +} + +/* + Represents a condition that can be either TRUE=1 (selects all elements) or FALSE=0(selects no elements). + Microsoft documentation: +*/ +class IUIAutomationBoolCondition extends UIA.IUIAutomationCondition { + static __IID := "{1B4E1F2E-75EB-4D0B-8952-5A69988E2307}" + Value { + get { + local boolVal + return (ComCall(3, this, "int*", &boolVal := 0), boolVal) + } + } +} + +/* + Represents a condition that is the negative of another condition. + Microsoft documentation: https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nn-uiautomationclient-iuiautomationnotcondition +*/ +class IUIAutomationNotCondition extends UIA.IUIAutomationCondition { + static __IID := "{f528b657-847b-498c-8896-d52b565407a1}" + GetChild() { + local condition + return (ComCall(3, this, "ptr*", &condition := 0), UIA.IUIAutomationCondition.__QueryCondition(condition)) + } +} + +class IUIAutomationCondition extends UIA.IUIAutomationBase { + static __QueryCondition(pCond) { + for n in ["Property", "Bool", "And", "Or", "Not"] { + try { + if ComObjQuery(pCond, UIA.IUIAutomation%n%Condition.__IID) + return UIA.IUIAutomation%n%Condition(pCond) + } + } + } + static __SafeArrayToConditionArray(pSafeArr) { + local safeArray := ComValue(0x2003,pSafeArr), out := [], k, cond + for k in safeArray { + if cond := UIA.IUIAutomationCondition.__QueryCondition(k) + out.Push(cond) + } + return out + } +} + +/* + Exposes properties and methods of a cache request. Client applications use this interface to specify the properties and control patterns to be cached when a Microsoft UI Automation element is obtained. + Microsoft documentation: https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nn-uiautomationclient-iuiautomationcacherequest +*/ +class IUIAutomationCacheRequest extends UIA.IUIAutomationBase { + ; Adds a property to the cache request. + AddProperty(propertyId) => ComCall(3, this, "int", UIA.TypeValidation.Property(propertyId)) + + ; Adds a control pattern to the cache request. Adding a control pattern that is already in the cache request has no effect. + AddPattern(patternId) => ComCall(4, this, "int", UIA.TypeValidation.Pattern(patternId)) + + ; Creates a copy of the cache request. + Clone() { + local clonedRequest + if (ComCall(5, this, "ptr*", &clonedRequest := 0), clonedRequest) + return UIA.IUIAutomationCacheRequest(clonedRequest) + throw Error("Cloning the CacheRequest failed", -1) + } + + TreeScope { + get { + local scope + return (ComCall(6, this, "int*", &scope := 0), scope) + } + set => ComCall(7, this, "int", UIA.TypeValidation.TreeScope(value)) + } + + TreeFilter { + get { + local filter + if (ComCall(8, this, "ptr*", &filter := 0), filter) + return UIA.IUIAutomationCondition(filter) + throw UnsetError("No condition returned by " A_ThisFunc, -1) + } + set => ComCall(9, this, "ptr", (Type(Value) ~= "i)Condition|ComValue") ? value : UIA.__CreateRawCondition(value)) + } + + AutomationElementMode { + get { + local mode + return (ComCall(10, this, "int*", &mode := 0), mode) + } + set => ComCall(11, this, "int", UIA.TypeValidation.AutomationElementMode(Value)) + } + + ; Extracts all properties from an object condition and adds them to the CacheRequest + AddPropertiesFromCondition(condition) { + local value, prop + if Type(condition) = "Object" + condition := condition.OwnProps() + for prop, value in condition { + try prop := UIA.TypeValidation.Property(prop) + if !(IsInteger(prop) && prop > 10000) { + if IsObject(value) + this.AddPropertiesFromCondition(value) + } else + this.AddProperty(prop) + } + } +} + +/** + * Creates a new event handler, that when registered will call the supplied function on the registered event. + * @param funcObj The callback function that will get called on events. + * The function needs to accept a certain number of parameters that depends on which event will be registered. + * HandleAutomationEvent(sender, eventId) + * HandleFocusChangedEvent(sender) + * HandlePropertyChangedEvent(sender, propertyId, newValue) + * HandleStructureChangedEvent(sender, changeType, runtimeId) + * HandleTextEditTextChangedEvent(sender, changeType, eventStrings) + * HandleChangesEvent(sender, uiaChanges, changesCount) + * HandleNotificationEvent(sender, notificationKind, notificationProcessing, displayString, activityId) + * @param handlerType This needs to be provided if the handler is going to be used with something other than UIA.AddAutomationEventHandler() + * For UIA.AddAutomationEventHandler leave this empty (default value). + * For UIA.Add...EventHandler, specify the ... part: FocusChanged, StructureChanged, TextEditTextChanged, Changes, Notification. + * Eg. For UIA.AddFocusChangedEventHandler, set this value to "FocusChanged": CreateEventHandler(Func, "FocusChanged") + */ +static CreateEventHandler(funcObj, handlerType:="") { + local buf, handler, cR, cAF, cQI, cF + if funcObj is String + try funcObj := %funcObj% + if !HasMethod(funcObj, "Call") + throw TypeError("Invalid function provided", -2) + + buf := Buffer(A_PtrSize * 6) + handler := UIA.IUIAutomation%handlerType%EventHandler() + handler.Buffer := buf, handler.Ptr := buf.ptr + handlerFunc := handler.Handle%(handlerType ? handlerType : "Automation")%Event + NumPut("ptr", buf.Ptr + 2*A_PtrSize, "ptr", ObjPtr(handler), "ptr", cQI:=CallbackCreate(QueryInterface,, 3), "ptr", cAF:=CallbackCreate(AddRef,, 1), "ptr", cR:=CallbackCreate(Release,, 1), "ptr", cF:=CallbackCreate(handlerFunc.Bind(handler),, handlerFunc.MaxParams-1), buf) + ObjRelease(ObjPtr(handler)) ; CallbackCreate binds "handler" to itself, so decrease ref count by 1 to allow it to be released, then inside __Delete add it back before CallbackFree + handler.DefineProp("__Delete", { call: (this, *) => (ObjAddRef(ObjPtr(this)), CallbackFree(cQI), CallbackFree(cAF), CallbackFree(cR), CallbackFree(cF)) }) + handler.EventHandler := funcObj + + return handler + + QueryInterface(pSelf, pRIID, pObj){ ; Credit: https://github.com/neptercn/UIAutomation/blob/master/UIA.ahk + DllCall("ole32\StringFromIID","ptr",pRIID,"wstr*",&str) + return (str="{00000000-0000-0000-C000-000000000046}")||(str="{146c3c17-f12e-4e22-8c27-f894b9b79c69}")||(str="{40cd37d4-c756-4b0c-8c6f-bddfeeb13b50}")||(str="{e81d1b4e-11c5-42f8-9754-e7036c79f054}")||(str="{c270f6b5-5c69-4290-9745-7a7f97169468}")||(str="{92FAA680-E704-4156-931A-E32D5BB38F3F}")||(str="{58EDCA55-2C3E-4980-B1B9-56C17F27A2A0}")||(str="{C7CB2637-E6C2-4D0C-85DE-4948C02175C7}")?(NumPut("ptr",pSelf,pObj), ObjAddRef(pSelf))*0:0x80004002 ; E_NOINTERFACE + } + AddRef(pSelf) => ObjAddRef(NumGet(pSelf, A_PtrSize, "ptr")) + Release(pSelf) => ObjRelease(NumGet(pSelf, A_PtrSize, "ptr")) +} +static CreateAutomationEventHandler(funcObj) => UIA.CreateEventHandler(funcObj) +static CreateFocusChangedEventHandler(funcObj) => UIA.CreateEventHandler(funcObj, "FocusChanged") +static CreatePropertyChangedEventHandler(funcObj) => UIA.CreateEventHandler(funcObj, "PropertyChanged") +static CreateStructureChangedEventHandler(funcObj) => UIA.CreateEventHandler(funcObj, "StructureChanged") +static CreateTextEditTextChangedEventHandler(funcObj) => UIA.CreateEventHandler(funcObj, "TextEditTextChanged") +static CreateChangesEventHandler(funcObj) => UIA.CreateEventHandler(funcObj, "Changes") +static CreateNotificationEventHandler(funcObj) => UIA.CreateEventHandler(funcObj, "Notification") + +class IUIAutomationEventHandler { + static __IID := "{146c3c17-f12e-4e22-8c27-f894b9b79c69}" + + HandleAutomationEvent(pSelf, sender, eventId) { + ObjAddRef(sender), this.EventHandler.Call(UIA.IUIAutomationElement(sender), eventId&0xFFFFFFFF) + } +} +class IUIAutomationFocusChangedEventHandler { + static __IID := "{c270f6b5-5c69-4290-9745-7a7f97169468}" + + HandleFocusChangedEvent(pSelf, sender) { + ObjAddRef(sender), this.EventHandler.Call(UIA.IUIAutomationElement(sender)) + } +} + +/* + Exposes a method to handle Microsoft UI Automation events that occur when a property is changed. + Microsoft documentation: https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nn-uiautomationclient-iuiautomationpropertychangedeventhandler +*/ +class IUIAutomationPropertyChangedEventHandler { ; UNTESTED + ;~ http://msdn.microsoft.com/en-us/library/windows/desktop/ee696119(v=vs.85).aspx + static __IID := "{40cd37d4-c756-4b0c-8c6f-bddfeeb13b50}" + + HandlePropertyChangedEvent(pSelf, sender, propertyId, newValue) { + local val := ComValue(0x400C, newValue)[] + DllCall("oleaut32\VariantClear", "ptr", newValue) + ObjAddRef(sender), this.EventHandler.Call(UIA.IUIAutomationElement(sender), propertyId&0xFFFFFFFF, val) + } +} +/* + Exposes a method to handle events that occur when the Microsoft UI Automation tree structure is changed. + Microsoft documentation: https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nn-uiautomationclient-iuiautomationstructurechangedeventhandler +*/ +class IUIAutomationStructureChangedEventHandler { + ;~ http://msdn.microsoft.com/en-us/library/windows/desktop/ee696197(v=vs.85).aspx + static __IID := "{e81d1b4e-11c5-42f8-9754-e7036c79f054}" + + HandleStructureChangedEvent(pSelf, sender, changeType, runtimeId) { + ObjAddRef(sender), this.EventHandler.Call(UIA.IUIAutomationElement(sender), changeType&0xFFFFFFFF, UIA.SafeArrayToAHKArray(runtimeId)) + DllCall("oleaut32\VariantClear", "ptr", runtimeId) + } +} +/* + Exposes a method to handle events that occur when Microsoft UI Automation reports a text-changed event from text edit controls + Microsoft documentation: https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nn-uiautomationclient-iuiautomationtextedittextchangedeventhandler +*/ +class IUIAutomationTextEditTextChangedEventHandler { ; UNTESTED + ;~ http://msdn.microsoft.com/en-us/library/windows/desktop/dn302202(v=vs.85).aspx + static __IID := "{92FAA680-E704-4156-931A-E32D5BB38F3F}" + + HandleTextEditTextChangedEvent(pSelf, sender, changeType, eventStrings) { + local val := ComValue(0x400C, eventStrings)[] + DllCall("oleaut32\VariantClear", "ptr", eventStrings) + ObjAddRef(sender), this.EventHandler.Call(UIA.IUIAutomationElement(sender), changeType&0xFFFFFFFF, val) + } +} + +/* + Exposes a method to handle one or more Microsoft UI Automation change events + Microsoft documentation: https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nn-uiautomationclient-iuiautomationchangeseventhandler +*/ +class IUIAutomationChangesEventHandler { ; UNTESTED + static __IID := "{58EDCA55-2C3E-4980-B1B9-56C17F27A2A0}" + + HandleChangesEvent(pSelf, sender, uiaChanges, changesCount) { + local changes, pExtraInfo + changes := {id:NumGet(uiaChanges,"Int"), payload:ComValue(0x400C, pPayload := NumGet(uiaChanges,8,"uint64"))[], extraInfo:ComValue(0x400C, pExtraInfo := NumGet(uiaChanges,16+2*A_PtrSize,"uint64")[])} + DllCall("oleaut32\VariantClear", "ptr", pPayload), DllCall("oleaut32\VariantClear", "ptr", pExtraInfo) + ObjAddRef(sender), this.EventHandler.Call(UIA.IUIAutomationElement(sender), changes, changesCount) + } +} +/* + Exposes a method to handle Microsoft UI Automation notification events + Microsoft documentation: https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nn-uiautomationclient-iuiautomationnotificationeventhandler +*/ +class IUIAutomationNotificationEventHandler { + static __IID := "{C7CB2637-E6C2-4D0C-85DE-4948C02175C7}" + + HandleNotificationEvent(pSelf, sender, notificationKind, notificationProcessing, displayString, activityId) { + ObjAddRef(sender), this.EventHandler.Call(UIA.IUIAutomationElement(sender), notificationKind, notificationProcessing, UIA.BSTR(displayString), UIA.BSTR(activityId)) + } +} + +class IUIAutomationEventHandlerGroup extends UIA.IUIAutomationBase { + static __IID := "{C9EE12F2-C13B-4408-997C-639914377F4E}" + + AddActiveTextPositionChangedEventHandler(handler, scope:=0x4, cacheRequest:=0) => ComCall(3, this, "int", UIA.TypeValidation.TreeScope(scope), "ptr", UIA.TypeValidation.CacheRequest(cacheRequest), "ptr", handler) + AddAutomationEventHandler(handler, eventId, scope:=0x4, cacheRequest:=0) => ComCall(4, this, "uint", eventId, "int", UIA.TypeValidation.TreeScope(scope), "ptr", UIA.TypeValidation.CacheRequest(cacheRequest), "ptr", handler) + AddChangesEventHandler(handler, changeTypes, scope:=0x4, cacheRequest:=0) { + local k, v + if !IsObject(changeTypes) + changeTypes := [changeTypes] + nativeArray := UIA.NativeArray(0, changeTypes.Length, "int") + for k, v in changeTypes + NumPut("int", v, nativeArray, (k-1)*4) + return ComCall(5, this, "int", UIA.TypeValidation.TreeScope(scope), "ptr", nativeArray, "int", changeTypes.Length, "int", cacheRequest, "ptr", handler) + } + AddNotificationEventHandler(handler, scope:=0x4, cacheRequest:=0) => ComCall(6, this, "int", UIA.TypeValidation.TreeScope(scope), "ptr", UIA.TypeValidation.CacheRequest(cacheRequest), "ptr", handler) + AddPropertyChangedEventHandler(handler, propertyArray, scope:=0x1,cacheRequest:=0) { + local i, propertyId, SafeArray + if !IsObject(propertyArray) + propertyArray := [propertyArray] + SafeArray:=ComObjArray(0x3,propertyArray.Length) + for i,propertyId in propertyArray + SafeArray[i-1]:=propertyId + return ComCall(7, this, "int", UIA.TypeValidation.TreeScope(scope), "ptr", UIA.TypeValidation.CacheRequest(cacheRequest),"ptr", handler,"ptr", SafeArray) + } + AddStructureChangedEventHandler(handler, scope:=0x4, cacheRequest:=0) => ComCall(8, this "int", UIA.TypeValidation.TreeScope(scope), "ptr",cacheRequest, "ptr", handler) + AddTextEditTextChangedEventHandler(handler, textEditChangeType, scope:=0x4, cacheRequest:=0) => ComCall(9, this, "int", UIA.TypeValidation.TreeScope(scope), "int", UIA.TypeValidation.TextEditChangeType(textEditChangeType), "ptr", UIA.TypeValidation.CacheRequest(cacheRequest), "ptr", handler) +} + +/* + Provides access to the properties of an annotation in a document. + Microsoft documentation: https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nn-uiautomationclient-iuiautomationannotationpattern +*/ +class IUIAutomationAnnotationPattern extends UIA.IUIAutomationBase { + AnnotationTypeId { + get { + local retVal + return (ComCall(3, this, "int*", &retVal := 0), retVal) + } + } + AnnotationTypeName { + get { + local retVal + return (ComCall(4, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + Author { + get { + local retVal + return (ComCall(5, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + DateTime { + get { + local retVal + return (ComCall(6, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + Target { + get { + local retVal + if (ComCall(7, this, "ptr*", &retVal := 0), retVal) + return UIA.IUIAutomationElement(retVal) + throw UnsetError("No element returned by " A_ThisFunc, -1) + } + } + CachedAnnotationTypeId { + get { + local retVal + return (ComCall(8, this, "int*", &retVal := 0), retVal) + } + } + CachedAnnotationTypeName { + get { + local retVal + return (ComCall(9, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + CachedAuthor { + get { + local retVal + return (ComCall(10, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + CachedDateTime { + get { + local retVal + return (ComCall(11, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + CachedTarget { + get { + local retVal + if (ComCall(11, this, "ptr*", &retVal := 0), retVal) + return UIA.IUIAutomationElement(retVal) + throw UnsetError("No element returned by " A_ThisFunc, -1) + } + } +} + +/* + Exposes a method to support access by a Microsoft UI Automation client to controls that support a custom navigation order. + Microsoft documentation: https://learn.microsoft.com/en-us/windows/win32/api/uiautomationclient/nn-uiautomationclient-iuiautomationcustomnavigationpattern +*/ +class IUIAutomationCustomNavigationPattern extends UIA.IUIAutomationBase { + ; Gets the next element in the specified direction within the logical UI tree. + ; direction can be one of UIA.NavigateDirection values: Parent, NextSibling, PreviousSibling, FirstChild, LastChild + Navigate(direction) { + local pRetVal + if (ComCall(3, this, "int", UIA.TypeValidation.NavigateDirection(direction), "ptr*", &pRetVal := 0), pRetVal) + return UIA.IUIAutomationElement(pRetVal) + throw UnsetError("No element returned by " A_ThisFunc, -1) + } +} + +/* + Provides access to a control that enables child elements to be arranged horizontally and vertically, relative to each other. + Microsoft documentation: https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nn-uiautomationclient-iuiautomationdockpattern +*/ +class IUIAutomationDockPattern extends UIA.IUIAutomationBase { + static __IID := "{fde5ef97-1464-48f6-90bf-43d0948e86ec}" + + ; ---------- DockPattern properties ---------- + + ; Retrieves the `dock position` of this element within its docking container. + DockPosition { + get { + local retVal + return (ComCall(4, this, "int*", &retVal := 0), retVal) + } + } + ; Retrieves the `cached dock` position of this element within its docking container. + CachedDockPosition { + get { + local retVal + return (ComCall(5, this, "int*", &retVal := 0), retVal) + } + } + + ; ---------- DockPattern methods ---------- + + /** + * Sets the dock position of this element. + * @param dockPos One of UIA.DockPosition values: Top, Left, Bottom, Right, Fill, None + */ + SetDockPosition(dockPos) => ComCall(3, this, "int", UIA.TypeValidation.DockPosition(dockPos)) +} + +/* + Provides access to information exposed by a UI Automation provider for an element that can be dragged as part of a drag-and-drop operation. + Microsoft documentation: https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nn-uiautomationclient-iuiautomationdragpattern +*/ +class IUIAutomationDragPattern extends UIA.IUIAutomationBase { + static __IID := "{1DC7B570-1F54-4BAD-BCDA-D36A722FB7BD}" + + ; ---------- DragPattern properties ---------- + IsGrabbed { + get { + local retVal + return (ComCall(3, this, "int*", &retVal := 0), retVal) + } + } + CachedIsGrabbed { + get { + local retVal + return (ComCall(4, this, "int*", &retVal := 0), retVal) + } + } + DropEffect { + get { + local retVal + return (ComCall(5, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + CachedDropEffect { + get { + local retVal + return (ComCall(6, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + DropEffects { + get { + local retVal + return (ComCall(7, this, "ptr*", &retVal := 0), ComValue(0x2008, retVal)) + } + } + CachedDropEffects { + get { + local retVal + return (ComCall(8, this, "ptr*", &retVal := 0), ComValue(0x2008, retVal)) + } + } + + ; ---------- DragPattern methods ---------- + /** + * Retrieves a collection of elements that represent the full set of items that the user is dragging as part of a drag operation. + * @returns {IUIAutomationElementArray} + */ + GetGrabbedItems() { + local retVal + return (ComCall(9, this, "ptr*", &retVal := 0), retVal?UIA.IUIAutomationElementArray(retVal).ToArray():[]) + } + GetCachedGrabbedItems() { + local retVal + return (ComCall(10, this, "ptr*", &retVal := 0), retVal?UIA.IUIAutomationElementArray(retVal).ToArray():[]) + } +} + +/* + Provides access to drag-and-drop information exposed by a Microsoft UI Automation provider for an element that can be the drop target of a drag-and-drop operation. + Microsoft documentation: https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nn-uiautomationclient-iuiautomationdroptargetpattern +*/ +class IUIAutomationDropTargetPattern extends UIA.IUIAutomationBase { + static __IID := "{69A095F7-EEE4-430E-A46B-FB73B1AE39A5}" + + ; ---------- DropTargetPattern properties ---------- + DropTargetEffect { + get { + local retVal + return (ComCall(3, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + CachedDropTargetEffect { + get { + local retVal + return (ComCall(4, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + DropTargetEffects { + get { + local retVal + return (ComCall(5, this, "ptr*", &retVal := 0), ComValue(0x2008, retVal)) + } + } + CachedDropTargetEffects { + get { + local retVal + return (ComCall(6, this, "ptr*", &retVal := 0), ComValue(0x2008, retVal)) + } + } +} + +class IUIAutomationExpandCollapsePattern extends UIA.IUIAutomationBase { + ; This is a blocking method that returns after the element has been collapsed. + ; There are cases when a element that is marked as a leaf node might not know whether it has children until either the IUIAutomationExpandCollapsePattern,,Collapse or the IUIAutomationExpandCollapsePattern,,Expand method is called. This behavior is possible with a tree view control that does delayed loading of its child items. For example, Microsoft Windows Explorer might display the expand icon for a node even though there are currently no child items; when the icon is clicked, the control polls for child items, finds none, and removes the expand icon. In these cases clients should listen for a property-changed event on the IUIAutomationExpandCollapsePattern,,ExpandCollapseState property. + + ; Displays all child nodes, controls, or content of the element. + Expand() => ComCall(3, this) + + ; Hides all child nodes, controls, or content of the element. + Collapse() => ComCall(4, this) + + ; Retrieves a value that indicates the state, expanded or collapsed, of the element. + ; Retrieve the name of the state via UIA.ExpandCollapseState[value] + ExpandCollapseState { + get { + local retVal + return (ComCall(5, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves a cached value that indicates the state, expanded or collapsed, of the element. + CachedExpandCollapseState { + get { + local retVal + return (ComCall(6, this, "int*", &retVal := 0), retVal) + } + } +} + +class IUIAutomationGridItemPattern extends UIA.IUIAutomationBase { + ; Retrieves the element that contains the grid item. + ContainingGrid { + get { + local retVal + if (ComCall(3, this, "ptr*", &retVal := 0), retVal) + return UIA.IUIAutomationElement(retVal) + throw UnsetError("No element returned by " A_ThisFunc, -1) + } + } + + ; Retrieves the zero-based index of the row that contains the grid item. + Row { + get { + local retVal + return (ComCall(4, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves the zero-based index of the column that contains the item. + Column { + get { + local retVal + return (ComCall(5, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves the number of rows spanned by the grid item. + RowSpan { + get { + local retVal + return (ComCall(6, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves the number of columns spanned by the grid item. + ColumnSpan { + get { + local retVal + return (ComCall(7, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves the cached element that contains the grid item. + CachedContainingGrid { + get { + local retVal + if (ComCall(8, this, "ptr*", &retVal := 0), retVal) + return UIA.IUIAutomationElement(retVal) + throw UnsetError("No element returned by " A_ThisFunc, -1) + } + } + + ; Retrieves the cached zero-based index of the row that contains the item. + CachedRow { + get { + local retVal + return (ComCall(9, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves the cached zero-based index of the column that contains the grid item. + CachedColumn { + get { + local retVal + return (ComCall(10, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves the cached number of rows spanned by a grid item. + CachedRowSpan { + get { + local retVal + return (ComCall(11, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves the cached number of columns spanned by the grid item. + CachedColumnSpan { + get { + local retVal + return (ComCall(12, this, "int*", &retVal := 0), retVal) + } + } +} + +class IUIAutomationGridPattern extends UIA.IUIAutomationBase { + ; Retrieves a UI Automation element representing an item in the grid. + GetItem(row, column) { + local element + if (ComCall(3, this, "int", row, "int", column, "ptr*", &element := 0), element) + return UIA.IUIAutomationElement(element) + throw UnsetError("No element returned by " A_ThisFunc, -1) + } + + ; Hidden rows and columns, depending on the provider implementation, may be loaded in the Microsoft UI Automation tree and will therefore be reflected in the row count and column count properties. If the hidden rows and columns have not yet been loaded they are not counted. + + ; Retrieves the number of rows in the grid. + RowCount { + get { + local retVal + return (ComCall(4, this, "int*", &retVal := 0), retVal) + } + } + + ; The number of columns in the grid. + ColumnCount { + get { + local retVal + return (ComCall(5, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves the cached number of rows in the grid. + CachedRowCount { + get { + local retVal + return (ComCall(6, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves the cached number of columns in the grid. + CachedColumnCount { + get { + local retVal + return (ComCall(7, this, "int*", &retVal := 0), retVal) + } + } +} + +class IUIAutomationInvokePattern extends UIA.IUIAutomationBase { + ; Invokes the action of a control, such as a button click. + ; Calls to this method should return immediately without blocking. However, this behavior depends on the implementation. + Invoke() => ComCall(3, this) +} + +class IUIAutomationItemContainerPattern extends UIA.IUIAutomationBase { + ; IUIAutomationItemContainerPattern + + ; Retrieves an element within a containing element, based on a specified property value. + ; The provider may return an actual UIA.IUIAutomationElement interface or a placeholder if the matching element is virtualized. + ; This method returns E_INVALIDARG if the property requested is not one that the container supports searching over. It is expected that most containers will support Name property, and if appropriate for the container, AutomationId and IsSelected. + ; This method can be slow, because it may need to traverse multiple objects to find a matching one. When used in a loop to return multiple items, no specific order is defined so long as each item is returned only once (that is, the loop should terminate). This method is also item-centric, not UI-centric, so items with multiple UI representations need to be hit only once. + ; When the propertyId parameter is specified as 0 (zero), the provider is expected to return the next item after pStartAfter. If pStartAfter is specified as NULL with a propertyId of 0, the provider should return the first item in the container. When propertyId is specified as 0, the value parameter should be VT_EMPTY. + FindItemByProperty(pStartAfter, propertyId, value) { + if A_PtrSize = 4 + value := UIA.ComVar(value, , true), ComCall(3, this, "ptr", pStartAfter, "int", propertyId, "int64", NumGet(value, "int64"), "int64", NumGet(value, 8, "int64"), "ptr*", &pFound := 0) + else + ComCall(3, this, "ptr", pStartAfter, "int", propertyId, "ptr", UIA.ComVar(value, , true), "ptr*", &pFound := 0) + if (pFound) + return UIA.IUIAutomationElement(pFound) + } +} + +class IUIAutomationLegacyIAccessiblePattern extends UIA.IUIAutomationBase { + + ; IUIAutomationLegacyIAccessiblePattern + + ; Performs a Microsoft Active Accessibility selection. + Select(flagsSelect:=3) => ComCall(3, this, "int", flagsSelect) + + ; Performs the Microsoft Active Accessibility default action for the element. + DoDefaultAction() => ComCall(4, this) + + ; Sets the Microsoft Active Accessibility value property for the element. This method is supported only for some elements (usually edit controls). + SetValue(szValue) => ComCall(5, this, "wstr", szValue) + + ; Retrieves the Microsoft Active Accessibility child identifier for the element. If the element is not a child element, CHILDID_SELF (0) is returned. + ChildId { + get { + local pRetVal + return (ComCall(6, this, "int*", &pRetVal := 0), pRetVal) + } + } + + ; Retrieves the Microsoft Active Accessibility name property of the element. The name of an element can be used to find the element in the element tree when the automation ID property is not supported on the element. + Name { + get { + local pszName + return (ComCall(7, this, "ptr*", &pszName := 0), UIA.BSTR(pszName)) + } + } + + ; Retrieves the Microsoft Active Accessibility value property. + Value { + get { + local pszValue + return (ComCall(8, this, "ptr*", &pszValue := 0), UIA.BSTR(pszValue)) + } + set => this.SetValue(value) + } + + ; Retrieves the Microsoft Active Accessibility description of the element. + Description { + get { + local pszDescription + return (ComCall(9, this, "ptr*", &pszDescription := 0), UIA.BSTR(pszDescription)) + } + } + + ; Retrieves the Microsoft Active Accessibility role identifier of the element. + Role { + get { + local pdwRole + return (ComCall(10, this, "uint*", &pdwRole := 0), pdwRole) + } + } + + ; Retrieves the Microsoft Active Accessibility state identifier for the element. + State { + get { + local pdwState + return (ComCall(11, this, "uint*", &pdwState := 0), pdwState) + } + } + + ; Retrieves the Microsoft Active Accessibility help string for the element. + Help { + get { + local pszHelp + return (ComCall(12, this, "ptr*", &pszHelp := 0), UIA.BSTR(pszHelp)) + } + } + + ; Retrieves the Microsoft Active Accessibility keyboard shortcut property for the element. + KeyboardShortcut { + get { + local pszKeyboardShortcut + return (ComCall(13, this, "ptr*", &pszKeyboardShortcut := 0), UIA.BSTR(pszKeyboardShortcut)) + } + } + + ; Retrieves the Microsoft Active Accessibility property that identifies the selected children of this element. + GetSelection() { + local pvarSelectedChildren + return (ComCall(14, this, "ptr*", &pvarSelectedChildren := 0), pvarSelectedChildren?UIA.IUIAutomationElementArray(pvarSelectedChildren).ToArray():[]) + } + + ; Retrieves the Microsoft Active Accessibility default action for the element. + DefaultAction { + get { + local pszDefaultAction + return (ComCall(15, this, "ptr*", &pszDefaultAction := 0), UIA.BSTR(pszDefaultAction)) + } + } + + ; Retrieves the cached Microsoft Active Accessibility child identifier for the element. + CachedChildId { + get { + local pRetVal + return (ComCall(16, this, "int*", &pRetVal := 0), pRetVal) + } + } + + ; Retrieves the cached Microsoft Active Accessibility name property of the element. + CachedName { + get { + local pszName + return (ComCall(17, this, "ptr*", &pszName := 0), UIA.BSTR(pszName)) + } + } + + ; Retrieves the cached Microsoft Active Accessibility value property. + CachedValue { + get { + local pszValue + return (ComCall(18, this, "ptr*", &pszValue := 0), UIA.BSTR(pszValue)) + } + } + + ; Retrieves the cached Microsoft Active Accessibility description of the element. + CachedDescription { + get { + local pszDescription + return (ComCall(19, this, "ptr*", &pszDescription := 0), UIA.BSTR(pszDescription)) + } + } + + ; Retrieves the cached Microsoft Active Accessibility role of the element. + CachedRole { + get { + local pdwRole + return (ComCall(20, this, "uint*", &pdwRole := 0), pdwRole) + } + } + + ; Retrieves the cached Microsoft Active Accessibility state identifier for the element. + CachedState { + get { + local pdwState + return (ComCall(21, this, "uint*", &pdwState := 0), pdwState) + } + } + + ; Retrieves the cached Microsoft Active Accessibility help string for the element. + CachedHelp { + get { + local pszHelp + return (ComCall(22, this, "ptr*", &pszHelp := 0), UIA.BSTR(pszHelp)) + } + } + + ; Retrieves the cached Microsoft Active Accessibility keyboard shortcut property for the element. + CachedKeyboardShortcut { + get { + local pszKeyboardShortcut + return (ComCall(23, this, "ptr*", &pszKeyboardShortcut := 0), UIA.BSTR(pszKeyboardShortcut)) + } + } + + ; Retrieves the cached Microsoft Active Accessibility property that identifies the selected children of this element. + GetCachedSelection() { + local pvarSelectedChildren + return (ComCall(24, this, "ptr*", &pvarSelectedChildren := 0), pvarSelectedChildren?UIA.IUIAutomationElementArray(pvarSelectedChildren).ToArray():[]) + } + + ; Retrieves the Microsoft Active Accessibility default action for the element. + CachedDefaultAction { + get { + local pszDefaultAction + return (ComCall(25, this, "ptr*", &pszDefaultAction := 0), UIA.BSTR(pszDefaultAction)) + } + } + + ; Retrieves an IAccessible object that corresponds to the Microsoft UI Automation element. + ; This method returns NULL if the underlying implementation of the UI Automation element is not a native Microsoft Active Accessibility server; that is, if a client attempts to retrieve the IAccessible interface for an element originally supported by a proxy object from OLEACC.dll, or by the UIA-to-MSAA Bridge. + GetIAccessible() { + local ppAccessible + return (ComCall(26, this, "ptr*", &ppAccessible := 0), ComValue(9, ppAccessible)) + } +} + +class IUIAutomationMultipleViewPattern extends UIA.IUIAutomationBase { + static __IID := "{8d253c91-1dc5-4bb5-b18f-ade16fa495e8}" + + ; ---------- MultipleViewPattern properties ---------- + + ; Retrieves the name of a control-specific view. + GetViewName(view) { + local name + return (ComCall(3, this, "int", view, "ptr*", &name := 0), UIA.BSTR(name)) + } + + ; Sets the view of the control. + SetView(view) => ComCall(4, this, "int", view) + + ; Retrieves the control-specific identifier of the current view of the control. + View => this.CurrentView + CurrentView { + get { + local retVal + return (ComCall(5, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves a collection of control-specific view identifiers. + GetSupportedViews() { + local retVal + return (ComCall(6, this, "ptr*", &retVal := 0), UIA.SafeArrayToAHKArray(retVal)) + } + + ; Retrieves the cached control-specific identifier of the current view of the control. + CachedView => this.CachedCurrentView + CachedCurrentView { + get { + local retVal + return (ComCall(7, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves a collection of control-specific view identifiers from the cache. + GetCachedSupportedViews() { + local retVal + return (ComCall(8, this, "ptr*", &retVal := 0), UIA.SafeArrayToAHKArray(retVal)) + } +} + +/* + Provides access to the underlying object model implemented by a control or application. + Microsoft documentation: https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nn-uiautomationclient-iuiautomationobjectmodelpattern +*/ +class IUIAutomationObjectModelPattern extends UIA.IUIAutomationBase { + static __IID := "{71c284b3-c14d-4d14-981e-19751b0d756d}" + ; Retrieves an interface used to access the underlying object model of the provider. + GetUnderlyingObjectModel() { + local retVal + return (ComCall(3, this, "ptr*", &retVal := 0), ComValue(0xd, retVal)) + } +} + +class IUIAutomationProxyFactory extends UIA.IUIAutomationBase { + CreateProvider(hwnd, idObject, idChild) { + local provider + return (ComCall(3, this, "ptr", hwnd, "int", idObject, "int", idChild, "ptr*", &provider := 0), ComValue(0xd, provider)) + } + ProxyFactoryId { + get { + local factoryId + return (ComCall(4, this, "ptr*", &factoryId := 0), UIA.BSTR(factoryId)) + } + } +} + +class IUIAutomationProxyFactoryEntry extends UIA.IUIAutomationBase { + ProxyFactory() { + local factory + if (ComCall(3, this, "ptr*", &factory := 0), factory) + return UIA.IUIAutomationProxyFactory(factory) + throw UnsetError("No ProxyFactory returned by " A_ThisFunc, -1) + } + ClassName { + get { + local classname + return (ComCall(4, this, "ptr*", &classname := 0), UIA.BSTR(classname)) + } + set => (ComCall(9, this, "wstr", Value)) + } + ImageName { + get { + local imageName + return (ComCall(5, this, "ptr*", &imageName := 0), UIA.BSTR(imageName)) + } + set => (ComCall(10, this, "wstr", Value)) + } + AllowSubstringMatch { + get { + local allowSubstringMatch + return (ComCall(6, this, "int*", &allowSubstringMatch := 0), allowSubstringMatch) + } + set => (ComCall(11, this, "int", Value)) + } + CanCheckBaseClass { + get { + local canCheckBaseClass + return (ComCall(7, this, "int*", &canCheckBaseClass := 0), canCheckBaseClass) + } + set => (ComCall(12, this, "int", Value)) + } + NeedsAdviseEvents { + get { + local adviseEvents + return (ComCall(8, this, "int*", &adviseEvents := 0), adviseEvents) + } + set => (ComCall(13, this, "int", Value)) + } + SetWinEventsForAutomationEvent(eventId, propertyId, winEvents) => ComCall(14, this, "int", eventId, "Int", propertyId, "ptr", winEvents) + GetWinEventsForAutomationEvent(eventId, propertyId) { + local winEvents + return (ComCall(15, this, "int", eventId, "Int", propertyId, "ptr*", &winEvents := 0), ComValue(0x200d, winEvents)) + } +} + +class IUIAutomationProxyFactoryMapping extends UIA.IUIAutomationBase { + Count { + get { + local count + return (ComCall(3, this, "uint*", &count := 0), count) + } + } + GetTable() { + local table + return (ComCall(4, this, "ptr*", &table := 0), ComValue(0x200d, table)) + } + GetEntry(index) { + local entry + if (ComCall(5, this, "int", index, "ptr*", &entry := 0), entry) + return UIA.IUIAutomationProxyFactoryEntry(entry) + throw UnsetError("No ProxyFactoryEntry returned by " A_ThisFunc, -1) + } + SetTable(factoryList) => ComCall(6, this, "ptr", factoryList) + InsertEntries(before, factoryList) => ComCall(7, this, "uint", before, "ptr", factoryList) + InsertEntry(before, factory) => ComCall(8, this, "uint", before, "ptr", factory) + RemoveEntry(index) => ComCall(9, this, "uint", index) + ClearTable() => ComCall(10, this) + RestoreDefaultTable() => ComCall(11, this) +} + +class IUIAutomationRangeValuePattern extends UIA.IUIAutomationBase { + ; Sets the value of the control. + SetValue(val) => ComCall(3, this, "double", val) + + ; Retrieves the value of the control. + Value { + get { + local retVal + return (ComCall(4, this, "double*", &retVal := 0), retVal) + } + set => ComCall(3, this, "double", value) + } + + ; Indicates whether the value of the element can be changed. + IsReadOnly { + get { + local retVal + return (ComCall(5, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves the maximum value of the control. + Maximum { + get { + local retVal + return (ComCall(6, this, "double*", &retVal := 0), retVal) + } + } + + ; Retrieves the minimum value of the control. + Minimum { + get { + local retVal + return (ComCall(7, this, "double*", &retVal := 0), retVal) + } + } + + ; The LargeChange and SmallChange property can support a Not a Number (NaN) value. When retrieving this property, a client can use the _isnan function to determine whether the property is a NaN value. + + ; Retrieves the value that is added to or subtracted from the value of the control when a large change is made, such as when the PAGE DOWN key is pressed. + LargeChange { + get { + local retVal + return (ComCall(8, this, "double*", &retVal := 0), retVal) + } + } + + ; Retrieves the value that is added to or subtracted from the value of the control when a small change is made, such as when an arrow key is pressed. + SmallChange { + get { + local retVal + return (ComCall(9, this, "double*", &retVal := 0), retVal) + } + } + + ; Retrieves the cached value of the control. + CachedValue { + get { + local retVal + return (ComCall(10, this, "double*", &retVal := 0), retVal) + } + } + + ; Retrieves a cached value that indicates whether the value of the element can be changed. + CachedIsReadOnly { + get { + local retVal + return (ComCall(11, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves the cached maximum value of the control. + CachedMaximum { + get { + local retVal + return (ComCall(12, this, "double*", &retVal := 0), retVal) + } + } + + ; Retrieves the cached minimum value of the control. + CachedMinimum { + get { + local retVal + return (ComCall(13, this, "double*", &retVal := 0), retVal) + } + } + + ; Retrieves, from the cache, the value that is added to or subtracted from the value of the control when a large change is made, such as when the PAGE DOWN key is pressed. + CachedLargeChange { + get { + local retVal + return (ComCall(14, this, "double*", &retVal := 0), retVal) + } + } + + ; Retrieves, from the cache, the value that is added to or subtracted from the value of the control when a small change is made, such as when an arrow key is pressed. + CachedSmallChange { + get { + local retVal + return (ComCall(15, this, "double*", &retVal := 0), retVal) + } + } +} + +class IUIAutomationScrollItemPattern extends UIA.IUIAutomationBase { + ; Scrolls the content area of a container object to display the UI Automation element within the visible region (viewport) of the container. + ; This method does not provide the ability to specify the position of the element within the viewport. + ScrollIntoView() => ComCall(3, this) +} + +class IUIAutomationScrollPattern extends UIA.IUIAutomationBase { + ; Scrolls the visible region of the content area horizontally and vertically. + ; Default values for horizontalAmount and horizontalAmount is UIA.ScrollAmount.NoAmount + Scroll(verticalAmount:=-1, horizontalAmount:=-1) => ComCall(3, this, "int", UIA.TypeValidation.ScrollAmount(horizontalAmount), "int", UIA.TypeValidation.ScrollAmount(verticalAmount)) + + ; Sets the horizontal and vertical scroll positions as a percentage of the total content area within the UI Automation element. + ; This method is useful only when the content area of the control is larger than the visible region. + ; Default values for horizontalPercent and verticalPercent is UIA.ScrollAmount.NoAmount + SetScrollPercent(verticalPercent:=-1, horizontalPercent:=-1) => ComCall(4, this, "double", horizontalPercent, "double", verticalPercent) + + ; Retrieves the horizontal scroll position. + HorizontalScrollPercent { + get { + local retVal + return (ComCall(5, this, "double*", &retVal := 0), retVal) + } + set => this.SetScrollPercent(,value) + } + + ; Retrieves the vertical scroll position. + VerticalScrollPercent { + get { + local retVal + return (ComCall(6, this, "double*", &retVal := 0), retVal) + } + set => this.SetScrollPercent(value) + } + + ; Retrieves the horizontal size of the viewable region of a scrollable element. + HorizontalViewSize { + get { + local retVal + return (ComCall(7, this, "double*", &retVal := 0), retVal) + } + } + + ; Retrieves the vertical size of the viewable region of a scrollable element. + VerticalViewSize { + get { + local retVal + return (ComCall(8, this, "double*", &retVal := 0), retVal) + } + } + + ; Indicates whether the element can scroll horizontally. + ; This property can be dynamic. For example, the content area of the element might not be larger than the current viewable area, meaning that the property is FALSE. However, resizing the element or adding child items can increase the bounds of the content area beyond the viewable area, making the property TRUE. + HorizontallyScrollable { + get { + local retVal + return (ComCall(9, this, "int*", &retVal := 0), retVal) + } + } + + ; Indicates whether the element can scroll vertically. + VerticallyScrollable { + get { + local retVal + return (ComCall(10, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves the cached horizontal scroll position. + CachedHorizontalScrollPercent { + get { + local retVal + return (ComCall(11, this, "double*", &retVal := 0), retVal) + } + } + + ; Retrieves the cached vertical scroll position. + CachedVerticalScrollPercent { + get { + local retVal + return (ComCall(12, this, "double*", &retVal := 0), retVal) + } + } + + ; Retrieves the cached horizontal size of the viewable region of a scrollable element. + CachedHorizontalViewSize { + get { + local retVal + return (ComCall(13, this, "double*", &retVal := 0), retVal) + } + } + + ; Retrieves the cached vertical size of the viewable region of a scrollable element. + CachedVerticalViewSize { + get { + local retVal + return (ComCall(14, this, "double*", &retVal := 0), retVal) + } + } + + ; Retrieves a cached value that indicates whether the element can scroll horizontally. + CachedHorizontallyScrollable { + get { + local retVal + return (ComCall(15, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves a cached value that indicates whether the element can scroll vertically. + CachedVerticallyScrollable { + get { + local retVal + return (ComCall(16, this, "int*", &retVal := 0), retVal) + } + } +} + +class IUIAutomationSelectionItemPattern extends UIA.IUIAutomationBase { + ; Clears any selected items and then selects the current element. + Select() => ComCall(3, this) + + ; Adds the current element to the collection of selected items. + AddToSelection() => ComCall(4, this) + + ; Removes this element from the selection. + ; An error code is returned if this element is the only one in the selection and the selection container requires at least one element to be selected. + RemoveFromSelection() => ComCall(5, this) + + ; Indicates whether this item is selected. + IsSelected { + get { + local retVal + return (ComCall(6, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves the element that supports IUIAutomationSelectionPattern and acts as the container for this item. + SelectionContainer { + get { + local retVal + if (ComCall(7, this, "ptr*", &retVal := 0), retVal) + return UIA.IUIAutomationElement(retVal) + throw UnsetError("No element returned by " A_ThisFunc, -1) + } + } + + ; A cached value that indicates whether this item is selected. + CachedIsSelected { + get { + local retVal + return (ComCall(8, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves the cached element that supports IUIAutomationSelectionPattern and acts as the container for this item. + CachedSelectionContainer { + get { + local retVal + if (ComCall(9, this, "ptr*", &retVal := 0), retVal) + return UIA.IUIAutomationElement(retVal) + throw UnsetError("No element returned by " A_ThisFunc, -1) + } + } +} + +class IUIAutomationSelectionPattern extends UIA.IUIAutomationBase { + ; Retrieves the selected elements in the container. + GetSelection() { + local retVal + return (ComCall(3, this, "ptr*", &retVal := 0), retVal?UIA.IUIAutomationElementArray(retVal).ToArray():[]) + } + + ; Indicates whether more than one item in the container can be selected at one time. + CanSelectMultiple { + get { + local retVal + return (ComCall(4, this, "int*", &retVal := 0), retVal) + } + } + + ; Indicates whether at least one item must be selected at all times. + IsSelectionRequired { + get { + local retVal + return (ComCall(5, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves the cached selected elements in the container. + GetCachedSelection() { + local retVal + return (ComCall(6, this, "ptr*", &retVal := 0), retVal?UIA.IUIAutomationElementArray(retVal).ToArray():[]) + } + + ; Retrieves a cached value that indicates whether more than one item in the container can be selected at one time. + CachedCanSelectMultiple { + get { + local retVal + return (ComCall(7, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves a cached value that indicates whether at least one item must be selected at all times. + CachedIsSelectionRequired { + get { + local retVal + return (ComCall(8, this, "int*", &retVal := 0), retVal) + } + } + + ; --------------- SelectionPattern2 --------------- + + ; Gets an IUIAutomationElement object representing the first item in a group of selected items. + FirstSelectedItem { + get { + local retVal + return (ComCall(9, this, "int*", &retVal := 0), retVal) + } + } + + ; Gets an IUIAutomationElement object representing the last item in a group of selected items. + LastSelectedItem { + get { + local retVal + return (ComCall(10, this, "int*", &retVal := 0), retVal) + } + } + + ; Gets an IUIAutomationElement object representing the currently selected item. + SelectedItem { + get { + local retVal + return (ComCall(11, this, "int*", &retVal := 0), retVal) + } + } + + ; Gets an integer value indicating the number of selected items. + ItemCount { + get { + local retVal + return (ComCall(12, this, "int*", &retVal := 0), retVal) + } + } + + ; Gets a cached IUIAutomationElement object representing the first item in a group of selected items. + CachedFirstSelectedItem { + get { + local retVal + return (ComCall(13, this, "int*", &retVal := 0), retVal) + } + } + + ; Gets a cached IUIAutomationElement object representing the last item in a group of selected items. + CachedLastSelectedItem { + get { + local retVal + return (ComCall(14, this, "int*", &retVal := 0), retVal) + } + } + + ; Gets a cached IUIAutomationElement object representing the currently selected item. + CachedSelectedItem { + get { + local retVal + return (ComCall(15, this, "int*", &retVal := 0), retVal) + } + } + + ; Gets a cached integer value indicating the number of selected items. + CachedItemCount { + get { + local retVal + return (ComCall(16, this, "int*", &retVal := 0), retVal) + } + } +} + +class IUIAutomationSpreadsheetPattern extends UIA.IUIAutomationBase { + GetItemByName(name) { + local element + if (ComCall(3, this, "wstr", name, "ptr*", &element := 0), element) + return UIA.IUIAutomationElement(element) + throw UnsetError("No element returned by " A_ThisFunc, -1) + } +} + +class IUIAutomationSpreadsheetItemPattern extends UIA.IUIAutomationBase { + Formula { + get { + local retVal + return (ComCall(3, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + GetAnnotationObjects() { + local retVal + return (ComCall(4, this, "ptr*", &retVal := 0), retVal?UIA.IUIAutomationElementArray(retVal).ToArray():[]) + } + GetAnnotationTypes() { + local retVal + return (ComCall(5, this, "ptr*", &retVal := 0), ComValue(0x2003, retVal)) + } + CachedFormul { + get { + local retVal + return (ComCall(6, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + GetCachedAnnotationObjects() { + local retVal + return (ComCall(7, this, "ptr*", &retVal := 0), retVal?UIA.IUIAutomationElementArray(retVal).ToArray():[]) + } + GetCachedAnnotationTypes() { + local retVal + return (ComCall(8, this, "ptr*", &retVal := 0), ComValue(0x2003, retVal)) + } +} + +class IUIAutomationStylesPattern extends UIA.IUIAutomationBase { + StyleId { + get { + local retVal + return (ComCall(3, this, "int*", &retVal := 0), retVal) + } + } + StyleName { + get { + local retVal + return (ComCall(4, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + FillColor { + get { + local retVal + return (ComCall(5, this, "int*", &retVal := 0), retVal) + } + } + FillPatternStyle { + get { + local retVal + return (ComCall(6, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + Shape { + get { + local retVal + return (ComCall(7, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + FillPatternColor { + get { + local retVal + return (ComCall(8, this, "int*", &retVal := 0), retVal) + } + } + ExtendedProperties { + get { + local retVal + return (ComCall(9, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + GetExtendedPropertiesAsArray() { + local arr := [], p, propertyArray, propertyCount + ComCall(10, this, "ptr*", &propertyArray := 0, "int*", &propertyCount := 0) + for p in UIA.NativeArray(propertyArray, propertyCount) + arr.Push({ PropertyName: UIA.BSTR(NumGet(p, "ptr")), PropertyValue: UIA.BSTR(NumGet(p, A_PtrSize, "ptr")) }) + return arr + } + CachedStyleId { + get { + local retVal + return (ComCall(11, this, "int*", &retVal := 0), retVal) + } + } + CachedStyleName { + get { + local retVal + return (ComCall(12, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + CachedFillColor { + get { + local retVal + return (ComCall(13, this, "int*", &retVal := 0), retVal) + } + } + CachedFillPatternStyle { + get { + local retVal + return (ComCall(14, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + CachedShape { + get { + local retVal + return (ComCall(15, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + CachedFillPatternColor { + get { + local retVal + return (ComCall(16, this, "int*", &retVal := 0), retVal) + } + } + CachedExtendedProperties { + get { + local retVal + return (ComCall(17, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + GetCachedExtendedPropertiesAsArray() { + local arr := [], p, propertyArray, propertyCount + ComCall(18, this, "ptr*", &propertyArray := 0, "int*", &propertyCount := 0) + for p in UIA.NativeArray(propertyArray, propertyCount) + arr.Push({ PropertyName: UIA.BSTR(NumGet(p, "ptr")), PropertyValue: UIA.BSTR(NumGet(p, A_PtrSize, "ptr")) }) + return arr + } +} + +class IUIAutomationSynchronizedInputPattern extends UIA.IUIAutomationBase { + ; Causes the Microsoft UI Automation provider to start listening for mouse or keyboard input. + ; When matching input is found, the provider checks whether the target element matches the current element. If they match, the provider raises the UIA_InputReachedTargetEventId event; otherwise it raises the UIA_InputReachedOtherElementEventId or UIA_InputDiscardedEventId event. + ; After receiving input of the specified type, the provider stops checking for input and continues as normal. + ; If the provider is already listening for input, this method returns E_INVALIDOPERATION. + StartListening(inputType) => ComCall(3, this, "int", inputType) + + ; Causes the Microsoft UI Automation provider to stop listening for mouse or keyboard input. + Cancel() => ComCall(4, this) +} + +class IUIAutomationTableItemPattern extends UIA.IUIAutomationBase { + ; Retrieves the row headers associated with a table item or cell. + GetRowHeaderItems() { + local retVal + return (ComCall(3, this, "ptr*", &retVal := 0), retVal?UIA.IUIAutomationElementArray(retVal).ToArray():[]) + } + + ; Retrieves the column headers associated with a table item or cell. + GetColumnHeaderItems() { + local retVal + return (ComCall(4, this, "ptr*", &retVal := 0), retVal?UIA.IUIAutomationElementArray(retVal).ToArray():[]) + } + + ; Retrieves the cached row headers associated with a table item or cell. + GetCachedRowHeaderItems() { + local retVal + return (ComCall(5, this, "ptr*", &retVal := 0), retVal?UIA.IUIAutomationElementArray(retVal).ToArray():[]) + } + + ; Retrieves the cached column headers associated with a table item or cell. + GetCachedColumnHeaderItems() { + local retVal + return (ComCall(6, this, "ptr*", &retVal := 0), retVal?UIA.IUIAutomationElementArray(retVal).ToArray():[]) + } +} + +class IUIAutomationTablePattern extends UIA.IUIAutomationBase { + ; Retrieves a collection of UI Automation elements representing all the row headers in a table. + GetRowHeaders() { + local retVal + return (ComCall(3, this, "ptr*", &retVal := 0), retVal?UIA.IUIAutomationElementArray(retVal).ToArray():[]) + } + + ; Retrieves a collection of UI Automation elements representing all the column headers in a table. + GetColumnHeaders() { + local retVal + return (ComCall(4, this, "ptr*", &retVal := 0), retVal?UIA.IUIAutomationElementArray(retVal).ToArray():[]) + } + + ; Retrieves the primary direction of traversal for the table. + RowOrColumnMajor { + get { + local retVal + return (ComCall(5, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves a cached collection of UI Automation elements representing all the row headers in a table. + GetCachedRowHeaders() { + local retVal + return (ComCall(6, this, "ptr*", &retVal := 0), retVal?UIA.IUIAutomationElementArray(retVal).ToArray():[]) + } + + ; Retrieves a cached collection of UI Automation elements representing all the column headers in a table. + GetCachedColumnHeaders() { + local retVal + return (ComCall(7, this, "ptr*", &retVal := 0), retVal?UIA.IUIAutomationElementArray(retVal).ToArray():[]) + } + + ; Retrieves the cached primary direction of traversal for the table. + CachedRowOrColumnMajor { + get { + local retVal + return (ComCall(8, this, "int*", &retVal := 0), retVal) + } + } +} + +class IUIAutomationTextChildPattern extends UIA.IUIAutomationBase { + TextContainer { + get { + local container + if (ComCall(3, this, "ptr*", &container := 0), container) + return UIA.IUIAutomationElement(container) + throw UnsetError("No element returned by " A_ThisFunc, -1) + } + } + TextRange { + get { + local range + if (ComCall(4, this, "ptr*", &range := 0), range) + return UIA.IUIAutomationTextRange(range) + throw UnsetError("No TextRange returned by " A_ThisFunc, -1) + } + } +} + +class IUIAutomationTextEditPattern extends UIA.IUIAutomationBase { + GetActiveComposition() { + local range + if (ComCall(3, this, "ptr*", &range := 0), range) + return UIA.IUIAutomationTextRange(range) + throw UnsetError("No TextRange returned by " A_ThisFunc, -1) + } + GetConversionTarget() { + local range + if (ComCall(4, this, "ptr*", &range := 0), range) + return UIA.IUIAutomationTextRange(range) + throw UnsetError("No TextRange returned by " A_ThisFunc, -1) + } +} + +class IUIAutomationTextPattern extends UIA.IUIAutomationBase { + /* + * Retrieves the degenerate (empty) text range nearest to the specified screen coordinates. + * A text range that wraps a child object is returned if the screen coordinates are within the coordinates of an image, hyperlink, Microsoft Excel spreadsheet, or other embedded object. + * Because hidden text is not ignored, this method retrieves a degenerate range from the visible text closest to the specified coordinates. + */ + RangeFromPoint(x, y?) { + local range + if (ComCall(3, this, "int64", IsSet(y) ? y << 32 | (x & 0xFFFFFFFF) : x, "ptr*", &range := 0), range) + return UIA.IUIAutomationTextRange(range) + throw UnsetError("No TextRange returned by " A_ThisFunc, -1) + } + + ; Retrieves a text range enclosing a child element such as an image, hyperlink, Microsoft Excel spreadsheet, or other embedded object. + ; If there is no text in the range that encloses the child element, a degenerate (empty) range is returned. + ; The child parameter is either a child of the element associated with a IUIAutomationTextPattern or from the array of children of a IUIAutomationTextRange. + RangeFromChild(child) { + local range + if (ComCall(4, this, "ptr", child, "ptr*", &range := 0), range) + return UIA.IUIAutomationTextRange(range) + throw UnsetError("No TextRange returned by " A_ThisFunc, -1) + } + + ; Retrieves a collection of text ranges that represents the currently selected text in a text-based control. + ; If the control supports the selection of multiple, non-contiguous spans of text, the ranges collection receives one text range for each selected span. + ; If the control contains only a single span of selected text, the ranges collection receives a single text range. + ; If the control contains a text insertion point but no text is selected, the ranges collection receives a degenerate (empty) text range at the position of the text insertion point. + ; If the control does not contain a text insertion point or does not support text selection, ranges is set to NULL. + ; Use the IUIAutomationTextPattern.SupportedTextSelection property to test whether a control supports text selection. + GetSelection() { + local ranges + return (ComCall(5, this, "ptr*", &ranges := 0), ranges?UIA.IUIAutomationTextRangeArray(ranges).ToArray():[]) + } + + ; Retrieves the first selection text range that represents the currently selected text in a text-based control. + SelectionRange { + get => this.GetSelection()[1] + } + + ; Retrieves an array of disjoint text ranges from a text-based control where each text range represents a contiguous span of visible text. + ; If the visible text consists of one contiguous span of text, the ranges array will contain a single text range that represents all of the visible text. + ; If the visible text consists of multiple, disjoint spans of text, the ranges array will contain one text range for each visible span, beginning with the first visible span, and ending with the last visible span. Disjoint spans of visible text can occur when the content of a text-based control is partially obscured by an overlapping window or other object, or when a text-based control with multiple pages or columns has content that is partially scrolled out of view. + ; IUIAutomationTextPattern,,GetVisibleRanges retrieves a degenerate (empty) text range if no text is visible, if all text is scrolled out of view, or if the text-based control contains no text. + GetVisibleRanges() { + local ranges + return (ComCall(6, this, "ptr*", &ranges := 0), ranges?UIA.IUIAutomationTextRangeArray(ranges).ToArray():[]) + } + + ; Retrieves a text range that encloses the main text of a document. + ; Some auxiliary text such as headers, footnotes, or annotations might not be included. + DocumentRange { + get { + local range + if (ComCall(7, this, "ptr*", &range := 0), range) + return UIA.IUIAutomationTextRange(range) + throw UnsetError("No TextRange returned by " A_ThisFunc, -1) + } + } + + ; Retrieves a value that specifies the type of text selection that is supported by the control. + SupportedTextSelection { + get { + local supportedTextSelection + return (ComCall(8, this, "int*", &supportedTextSelection := 0), supportedTextSelection) + } + } + + ; ------------- TextPattern2 ------------ + + RangeFromAnnotation(annotation) { + local out + if (ComCall(9, this, "ptr", annotation, "ptr*", &out:=0), out) + return UIA.IUIAutomationTextRange(out) + throw UnsetError("No TextRange returned by " A_ThisFunc, -1) + } + GetCaretRange(&isActive:=0) { + local out + if (ComCall(10, this, "ptr*", &isActive:=0, "ptr*", &out:=0), out) + return UIA.IUIAutomationTextRange(out) + throw UnsetError("No TextRange returned by " A_ThisFunc, -1) + } +} + +/* + Provides access to a span of continuous text in a container that supports the TextPattern interface. TextRange can be used to select, compare, and retrieve embedded objects from the text span. The interface uses two endpoints to delimit where the text span starts and ends. Disjoint spans of text are represented by a TextRangeArray, which is an array of TextRange interfaces. + Microsoft documentation: https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nn-uiautomationclient-iuiautomationtextrange +*/ +class IUIAutomationTextRange extends UIA.IUIAutomationBase { + static __IID := "{A543CC6A-F4AE-494B-8239-C814481187A8}" + ; Retrieves a IUIAutomationTextRange identical to the original and inheriting all properties of the original. + ; The range can be manipulated independently of the original. + Clone() { + local clonedRange + if (ComCall(3, this, "ptr*", &clonedRange := 0), clonedRange) + return UIA.IUIAutomationTextRange(clonedRange) + throw UnsetError("No TextRange returned by " A_ThisFunc, -1) + } + + ; Retrieves a value that specifies whether this text range has the same endpoints as another text range. + ; This method compares the endpoints of the two text ranges, not the text in the ranges. The ranges are identical if they share the same endpoints. If two text ranges have different endpoints, they are not identical even if the text in both ranges is exactly the same. + Compare(range) { + local areSame + return (ComCall(4, this, "ptr", range, "int*", &areSame := 0), areSame) + } + + ; Retrieves a value that specifies whether the start or end endpoint of this text range is the same as the start or end endpoint of another text range. + ; EndPoint must be one of UIA.TextPatternRangeEndpoint values: Start, End + CompareEndpoints(srcEndPoint, range, targetEndPoint) { + local compValue + return (ComCall(5, this, "int", UIA.TypeValidation.TextPatternRangeEndpoint(srcEndPoint), "ptr", range, "int", UIA.TypeValidation.TextPatternRangeEndpoint(targetEndPoint), "int*", &compValue := 0), compValue) + } + + ; Normalizes the text range by the specified text unit. The range is expanded if it is smaller than the specified unit, or shortened if it is longer than the specified unit. + ; Client applications such as screen readers use this method to retrieve the full word, sentence, or paragraph that exists at the insertion point or caret position. + ; Despite its name, the ExpandToEnclosingUnit method does not necessarily expand a text range. Instead, it "normalizes" a text range by moving the endpoints so that the range encompasses the specified text unit. The range is expanded if it is smaller than the specified unit, or shortened if it is longer than the specified unit. If the range is already an exact quantity of the specified units, it remains unchanged. The following diagram shows how ExpandToEnclosingUnit normalizes a text range by moving the endpoints of the range. + ; ExpandToEnclosingUnit defaults to the next largest text unit supported if the specified text unit is not supported by the control. The order, from smallest unit to largest, is as follows, Character Format Word Line Paragraph Page Document + ; ExpandToEnclosingUnit respects both visible and hidden text. + ; TextUnit must be one of UIA.TextUnit values: Character, Format, Word, Line, Paragraph, Page, Document. Default is Document. + ExpandToEnclosingUnit(textUnit:=6) => ComCall(6, this, "int", textUnit) + + ; Retrieves a text range subset that has the specified text attribute value. + ; The FindAttribute method retrieves matching text regardless of whether the text is hidden or visible. Use UIA_IsHiddenAttributeId to check text visibility. + FindAttribute(attr, val, backward:=False) { + local found + if A_PtrSize = 4 + val := UIA.ComVar(val, , true), ComCall(7, this, "int", attr, "int64", NumGet(val, "int64"), "int64", NumGet(val, 8, "int64"), "int", backward, "ptr*", &found := 0) + else + ComCall(7, this, "int", attr, "ptr", UIA.ComVar(val, , true), "int", backward, "ptr*", &found := 0) + return found?UIA.IUIAutomationTextRange(found):"" + } + + ; Retrieves a text range subset that contains the specified text. There is no differentiation between hidden and visible text. + FindText(text, backward:=False, ignoreCase:=False) { + local found + if (ComCall(8, this, "wstr", text, "int", backward, "int", ignoreCase, "ptr*", &found := 0), found) + return UIA.IUIAutomationTextRange(found) + throw UnsetError("No TextRange returned by " A_ThisFunc, -1) + } + + ; Retrieves the value of the specified text attribute across the entire text range. + ; The type of value retrieved by this method depends on the attr parameter. For example, calling GetAttributeValue with the attr parameter set to UIA_FontNameAttributeId returns a string that represents the font name of the text range, while calling GetAttributeValue with attr set to UIA_IsItalicAttributeId would return a boolean. + ; If the attribute specified by attr is not supported, the value parameter receives a value that is equivalent to the IUIAutomation,,ReservedNotSupportedValue property. + ; A text range can include more than one value for a particular attribute. For example, if a text range includes more than one font, the FontName attribute will have multiple values. An attribute with more than one value is called a mixed attribute. You can determine if a particular attribute is a mixed attribute by comparing the value retrieved from GetAttributeValue with the UIAutomation,,ReservedMixedAttributeValue property. + ; The GetAttributeValue method retrieves the attribute value regardless of whether the text is hidden or visible. Use UIA_ IsHiddenAttributeId to check text visibility. + GetAttributeValue(attr) { + local val + return (ComCall(9, this, "int", attr, "ptr", val := UIA.ComVar()), val[]) + } + + ; Retrieves a collection of bounding rectangles for each fully or partially visible line of text in a text range. + GetBoundingRectangles() { + ComCall(10, this, "ptr*", &boundingRects := 0) + DllCall("oleaut32\SafeArrayGetVartype", "ptr", boundingRects, "ushort*", &baseType:=0) + retArr := [], sa := ComValue(0x2000 | baseType, boundingRects) + Loop sa.MaxIndex() / 4 + 1 + retArr.Push({x:Floor(sa[4*(A_Index-1)]),y:Floor(sa[4*(A_Index-1)+1]),w:Floor(sa[4*(A_Index-1)+2]),h:Floor(sa[4*(A_Index-1)+3])}) + return retArr + } + + ; Returns the innermost UI Automation element that encloses the text range. + GetEnclosingElement() { + local enclosingElement + if (ComCall(11, this, "ptr*", &enclosingElement := 0), enclosingElement) + return UIA.IUIAutomationElement(enclosingElement) + throw UnsetError("No element returned by " A_ThisFunc, -1) + } + + ; Returns the plain text of the text range. + GetText(maxLength := -1) { + local text + return (ComCall(12, this, "int", maxLength, "ptr*", &text := 0), UIA.BSTR(text)) + } + + ; Moves the text range forward or backward by the specified number of text units. + ; unit must be one of UIA.TextUnit values: Character, Format, Word, Line, Paragraph, Page, Document. + Move(unit, count) { + local moved + return (ComCall(13, this, "int", UIA.TypeValidation.TextUnit(unit), "int", count, "int*", &moved := 0), moved) + } + + ; Moves one endpoint of the text range the specified number of text units within the document range. + ; unit must be one of UIA.TextUnit values: Character, Format, Word, Line, Paragraph, Page, Document. Default is Document. + ; EndPoint must be one of UIA.TextPatternRangeEndpoint values: Start, End + MoveEndpointByUnit(endpoint, unit, count) { + local moved + return (ComCall(14, this, "int", UIA.TypeValidation.TextPatternRangeEndpoint(endpoint), "int", UIA.TypeValidation.TextUnit(unit), "int", count, "int*", &moved := 0), moved) + } + + ; Moves one endpoint of the current text range to the specified endpoint of a second text range. + ; If the endpoint being moved crosses the other endpoint of the same text range, that other endpoint is moved also, resulting in a degenerate (empty) range and ensuring the correct ordering of the endpoints (that is, the start is always less than or equal to the end). + ; EndPoint must be one of UIA.TextPatternRangeEndpoint values: Start, End + MoveEndpointByRange(srcEndPoint, range, targetEndPoint) { ; TextPatternRangeEndpoint , IUIAutomationTextRange , TextPatternRangeEndpoint + ComCall(15, this, "int", UIA.TypeValidation.TextPatternRangeEndpoint(srcEndPoint), "ptr", range, "int", UIA.TypeValidation.TextPatternRangeEndpoint(targetEndPoint)) + } + + ; Selects the span of text that corresponds to this text range, and removes any previous selection. + ; If the Select method is called on a text range object that represents a degenerate (empty) text range, the text insertion point moves to the starting endpoint of the text range. + Select() => ComCall(16, this) + + ; Adds the text range to the collection of selected text ranges in a control that supports multiple, disjoint spans of selected text. + ; The text insertion point moves to the newly selected text. If AddToSelection is called on a text range object that represents a degenerate (empty) text range, the text insertion point moves to the starting endpoint of the text range. + AddToSelection() => ComCall(17, this) + + ; Removes the text range from an existing collection of selected text in a text container that supports multiple, disjoint selections. + ; The text insertion point moves to the area of the removed highlight. Providing a degenerate text range also moves the insertion point. + RemoveFromSelection() => ComCall(18, this) + + ; Causes the text control to scroll until the text range is visible in the viewport. + ; The method respects both hidden and visible text. If the text range is hidden, the text control will scroll only if the hidden text has an anchor in the viewport. + ; A Microsoft UI Automation client can check text visibility by calling IUIAutomationTextRange,,GetAttributeValue with the attr parameter set to UIA_IsHiddenAttributeId. + ScrollIntoView(alignToTop:=0) => ComCall(19, this, "int", alignToTop) + + ; Retrieves a collection of all embedded objects that fall within the text range. + GetChildren() { + local children + return (ComCall(20, this, "ptr*", &children := 0), children?UIA.IUIAutomationElementArray(children).ToArray():"") + } + ; ---------- TextRange2 ---------- + + ShowContextMenu() => ComCall(21, this) + + ; ---------- TextRange3 ---------- + + GetEnclosingElementBuildCache(cacheRequest) { + local out + if (ComCall(22, this, "ptr", UIA.TypeValidation.CacheRequest(cacheRequest), "ptr*", &out:=0), out) + return UIA.IUIAutomationElement(out) + throw UnsetError("No element returned by " A_ThisFunc, -1) + } + GetChildrenBuildCache(cacheRequest) { + local out + return (ComCall(23, this, "ptr", UIA.TypeValidation.CacheRequest(cacheRequest), "ptr*", &out:=0), out?UIA.IUIAutomationElementArray(out).ToArray():[]) + } + GetAttributeValues(attributeIds, attributeIdCount) { + local out + ComCall(24, this, "ptr", ComObjValue(ComObjArray(8, attributeIds*)), "int", attributeIdCount, "ptr*", &out:=UIA.ComVar()) + return out[] + } +} + +class IUIAutomationTextRangeArray extends UIA.IUIAutomationArray { + ; Retrieves the number of text ranges in the collection. + Length { + get { + local length + return (ComCall(3, this, "int*", &length := 0), length) + } + } + + ; Retrieves a text range from the collection. + GetElement(index) { + local element + if (ComCall(4, this, "int", index, "ptr*", &element := 0), element) + return UIA.IUIAutomationTextRange(element) + throw UnsetError("No TextRange returned by " A_ThisFunc, -1) + } +} + +class IUIAutomationTogglePattern extends UIA.IUIAutomationBase { + ; Cycles through the toggle states of the control. + ; A control cycles through its states in this order, ToggleState_On, ToggleState_Off and, if supported, ToggleState_Indeterminate. + Toggle() => ComCall(3, this) + + ; Retrieves the state of the control. + ToggleState { + get { + local retVal + return (ComCall(4, this, "int*", &retVal := 0), retVal) + } + set { + if (this.ToggleState != value) + this.Toggle() + } + } + + ; Retrieves the cached state of the control. + CachedToggleState { + get { + local retVal + return (ComCall(5, this, "int*", &retVal := 0), retVal) + } + } +} + +/* + Provides access to a control that can be moved, resized, or rotated. + Microsoft documentation: https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nn-uiautomationclient-iuiautomationtransformpattern +*/ +class IUIAutomationTransformPattern extends UIA.IUIAutomationBase { + ; An element cannot be moved, resized or rotated such that its resulting screen location would be completely outside the coordinates of its container and inaccessible to the keyboard or mouse. For example, when a top-level window is moved completely off-screen or a child object is moved outside the boundaries of the container's viewport, the object is placed as close to the requested screen coordinates as possible with the top or left coordinates overridden to be within the container boundaries. + + ; Moves the UI Automation element. + Move(x, y) => ComCall(3, this, "double", x, "double", y) + + ; Resizes the UI Automation element. + ; When called on a control that supports split panes, this method can have the side effect of resizing other contiguous panes. + Resize(width, height) => ComCall(4, this, "double", width, "double", height) + + ; Rotates the UI Automation element. + Rotate(degrees) => ComCall(5, this, "double", degrees) + + ; Indicates whether the element can be moved. + CanMove { + get { + local retVal + return (ComCall(6, this, "int*", &retVal := 0), retVal) + } + } + + ; Indicates whether the element can be resized. + CanResize { + get { + local retVal + return (ComCall(7, this, "int*", &retVal := 0), retVal) + } + } + + ; Indicates whether the element can be rotated. + CanRotate { + get { + local retVal + return (ComCall(8, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves a cached value that indicates whether the element can be moved. + CachedCanMove { + get { + local retVal + return (ComCall(9, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves a cached value that indicates whether the element can be resized. + CachedCanResize { + get { + local retVal + return (ComCall(10, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves a cached value that indicates whether the element can be rotated. + CachedCanRotate { + get { + local retVal + return (ComCall(11, this, "int*", &retVal := 0), retVal) + } + } + + ; --------------- TransformPattern2 --------------- + + CanZoom { + get { + local retVal + return (ComCall(14, this, "int*", &retVal := 0), retVal) + } + } + CachedCanZoom { + get { + local retVal + return (ComCall(15, this, "int*", &retVal := 0), retVal) + } + } + ZoomLevel { + get { + local retVal + return (ComCall(16, this, "double*", &retVal := 0), retVal) + } + } + CachedZoomLevel { + get { + local retVal + return (ComCall(17, this, "double*", &retVal := 0), retVal) + } + } + ZoomMinimum { + get { + local retVal + return (ComCall(18, this, "double*", &retVal := 0), retVal) + } + } + CachedZoomMinimum { + get { + local retVal + return (ComCall(19, this, "double*", &retVal := 0), retVal) + } + } + ZoomMaximum { + get { + local retVal + return (ComCall(20, this, "double*", &retVal := 0), retVal) + } + } + CachedZoomMaximum { + get { + local retVal + return (ComCall(21, this, "double*", &retVal := 0), retVal) + } + } + Zoom(zoomValue) => ComCall(12, this, "double", zoomValue) + ZoomByUnit(ZoomUnit) => ComCall(13, this, "uint", ZoomUnit) +} + +class IUIAutomationValuePattern extends UIA.IUIAutomationBase { + ; Sets the value of the element. + ; The IsEnabled property must be TRUE, and the IUIAutomationValuePattern,,IsReadOnly property must be FALSE. + SetValue(val) => ComCall(3, this, "wstr", val) + + ; Retrieves the value of the element. + ; Single-line edit controls support programmatic access to their contents through IUIAutomationValuePattern. However, multiline edit controls do not support this control pattern, and their contents must be retrieved by using IUIAutomationTextPattern. + ; This property does not support the retrieval of formatting information or substring values. IUIAutomationTextPattern must be used in these scenarios as well. + Value { + get { + local retVal + return (ComCall(4, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + set => ComCall(3, this, "wstr", value) + } + + ; Indicates whether the value of the element is read-only. + IsReadOnly { + get { + local retVal + return (ComCall(5, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves the cached value of the element. + CachedValue { + get { + local retVal + return (ComCall(6, this, "ptr*", &retVal := 0), UIA.BSTR(retVal)) + } + } + + ; Retrieves a cached value that indicates whether the value of the element is read-only. + ; This property must be TRUE for IUIAutomationValuePattern,,SetValue to succeed. + CachedIsReadOnly { + get { + local retVal + return (ComCall(7, this, "int*", &retVal := 0), retVal) + } + } +} + +/* + Represents a virtualized item, which is an item that is represented by a placeholder automation element in the Microsoft UI Automation tree. + Microsoft documentation: https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nn-uiautomationclient-iuiautomationvirtualizeditempattern +*/ +class IUIAutomationVirtualizedItemPattern extends UIA.IUIAutomationBase { + ; Creates a full UI Automation element for a virtualized item. + ; A virtualized item is represented by a placeholder automation element in the UI Automation tree. The Realize method causes the provider to make full information available for the item so that a full UI Automation element can be created for the item. + Realize() => ComCall(3, this) +} + +/* + Provides access to the fundamental functionality of a window. + Microsoft documentation: https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nn-uiautomationclient-iuiautomationwindowpattern +*/ +class IUIAutomationWindowPattern extends UIA.IUIAutomationBase { + ; Closes the window. + ; When called on a split pane control, this method closes the pane and removes the associated split. This method may also close all other panes, depending on implementation. + Close() => ComCall(3, this) + + ; Causes the calling code to block for the specified time or until the associated process enters an idle state, whichever completes first. + WaitForInputIdle(milliseconds) { + local success + return (ComCall(4, this, "int", milliseconds, "int*", &success := 0), success) + } + + ; Minimizes, maximizes, or restores the window. + SetWindowVisualState(state) => ComCall(5, this, "int", state) + + ; Indicates whether the window can be maximized. + CanMaximize { + get { + local retVal + return (ComCall(6, this, "int*", &retVal := 0), retVal) + } + } + + ; Indicates whether the window can be minimized. + CanMinimize { + get { + local retVal + return (ComCall(7, this, "int*", &retVal := 0), retVal) + } + } + + ; Indicates whether the window is modal. + IsModal { + get { + local retVal + return (ComCall(8, this, "int*", &retVal := 0), retVal) + } + } + + ; Indicates whether the window is the topmost element in the z-order. + IsTopmost { + get { + local retVal + return (ComCall(9, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves the visual state of the window; that is, whether it is in the normal, maximized, or minimized state. + WindowVisualState { + get { + local retVal + return (ComCall(10, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves the current state of the window for the purposes of user interaction. + WindowInteractionState { + get { + local retVal + return (ComCall(11, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves a cached value that indicates whether the window can be maximized. + CachedCanMaximize { + get { + local retVal + return (ComCall(12, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves a cached value that indicates whether the window can be minimized. + CachedCanMinimize { + get { + local retVal + return (ComCall(13, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves a cached value that indicates whether the window is modal. + CachedIsModal { + get { + local retVal + return (ComCall(14, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves a cached value that indicates whether the window is the topmost element in the z-order. + CachedIsTopmost { + get { + local retVal + return (ComCall(15, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves a cached value that indicates the visual state of the window; that is, whether it is in the normal, maximized, or minimized state. + CachedWindowVisualState { + get { + local retVal + return (ComCall(16, this, "int*", &retVal := 0), retVal) + } + } + + ; Retrieves a cached value that indicates the current state of the window for the purposes of user interaction. + CachedWindowInteractionState { + get { + local retVal + return (ComCall(17, this, "int*", &retVal := 0), retVal) + } + } +} + +; Internal class: UIAViewer code +class Viewer { + static SettingsFolderPath := A_AppData "\UIAViewer" + static SettingsFilePath := A_AppData "\UIAViewer\settings.ini" + __New() { + local v, pattern, i + OnError this.ErrorHandler.Bind(this) + CoordMode "Mouse", "Screen" + DetectHiddenWindows "On" + this.Stored := {mwId:0, FilteredTreeView:Map(), TreeView:Map(), HighlightedElement:0} + this.Capturing := False, this.MacroSidebarVisible := False, this.MacroSidebarWidth := 350, this.MacroSidebarMinWidth := 290, this.GuiMinWidth := 540, this.GuiMinHeight := 400, this.Focused := 1 + this.LoadSettings() + this.cacheRequest := UIA.CreateCacheRequest() + ; Don't even get the live element, because we don't need it. Gives a significant speed improvement. + this.cacheRequest.AutomationElementMode := UIA.AutomationElementMode.None + ; Set TreeScope to include the starting element and all descendants as well + this.cacheRequest.TreeScope := 5 + + this.gViewer := Gui((this.AlwaysOnTop ? "AlwaysOnTop " : "") "Resize +MinSize" this.GuiMinWidth "x" this.GuiMinHeight, "UIAViewer") + this.gViewer.OnEvent("Close", this.gViewer_Close.Bind(this)) + this.gViewer.OnEvent("Size", this.gViewer_Size.Bind(this)) + (this.TextLVWin := this.gViewer.Add("Text", "w250", "Window Info")).SetFont("bold") + this.LVWin := this.gViewer.Add("ListView", "h135 w250", ["Property", "Value"]) + this.LVWin.OnEvent("ContextMenu", LV_CopyTextMethod := this.LV_CopyText.Bind(this)) + this.LVWin.ModifyCol(1,60) + this.LVWin.ModifyCol(2,180) + for v in ["Title", "Text", "Id", "Location", "Class(NN)", "Process", "PID"] + this.LVWin.Add(,v,"") + this.gViewer.Add("Text", "w100", "Properties").SetFont("bold") + this.LVProps := this.gViewer.Add("ListView", "h200 w250", ["Property", "Value"]) + this.LVProps.OnEvent("ContextMenu", LV_CopyTextMethod) + this.LVProps.ModifyCol(1,100) + this.LVProps.ModifyCol(2,140) + this.DisplayedProps := ["Type", "LocalizedType", "Name", "Value", "AutomationId", "BoundingRectangle", "ClassName", "FullDescription", "HelpText", "AccessKey", "AcceleratorKey", "HasKeyboardFocus", "IsKeyboardFocusable", "ItemType", "ProcessId", "IsEnabled", "IsPassword", "IsOffscreen", "FrameworkId", "IsRequiredForForm", "ItemStatus", "RuntimeId"] + Loop DisplayedPropsLength := this.DisplayedProps.Length { + v := this.DisplayedProps[i := DisplayedPropsLength-A_Index+1] + try this.cacheRequest.AddProperty(v) ; Throws if not available, + catch + this.DisplayedProps.RemoveAt(i) ; Remove property if it is not available + } + for v in this.DisplayedProps + this.LVProps.Add(,v = "BoundingRectangle" ? "Location" : v,"") + for pattern in [UIA.Property.OwnProps()*] { + if pattern ~= "Is([\w]+Pattern.?)Available" + try this.cacheRequest.AddProperty(UIA.Property.%pattern%) + catch + UIA.Property.DeleteProp(pattern) ; Remove pattern if it is not available + } + + (this.TextTVPatterns := this.gViewer.Add("Text", "w100 r1", "Patterns")).SetFont("bold") + this.TVPatterns := this.gViewer.Add("TreeView", "h85 w250") + this.TVPatterns.OnEvent("DoubleClick", this.TVPatterns_DoubleClick.Bind(this)) + + this.ButCapture := this.gViewer.Add("Button", "xp+60 y+10 w130", "Start capturing (Alt+S)") + this.ButCapture.OnEvent("Click", this.CaptureHotkeyFunc := this.ButCapture_Click.Bind(this)) + HotKey("!s", this.CaptureHotkeyFunc) + this.SBMain := this.gViewer.Add("StatusBar",, " Start capturing, then hold cursor still to construct tree") + this.SBMain.OnEvent("Click", this.SBMain_Click.Bind(this)) + this.SBMain.OnEvent("ContextMenu", this.SBMain_ContextMenu.Bind(this)) + (this.TextTVUIA := this.gViewer.Add("Text", "x278 y10 w600 r1", "UIA Tree")).SetFont("bold") + this.TVUIA := this.gViewer.Add("TreeView", "x275 y25 w300 h465") + this.TVUIA.OnEvent("Click", this.TVUIA_Click.Bind(this)) + this.TVUIA.OnEvent("ContextMenu", this.TVUIA_ContextMenu.Bind(this)) + this.TVUIA.Add("Start capturing to show tree") + this.TextFilterTVUIA := this.gViewer.Add("Text", "x275 y503", "&Filter:") + this.EditFilterTVUIA := this.gViewer.Add("Edit", "x305 y500 w100") + this.EditFilterTVUIA.OnEvent("Change", this.EditFilterTVUIA_Change.Bind(this)) + this.GroupBoxMacro := this.gViewer.Add("GroupBox", "x900 y20 w" (this.MacroSidebarWidth-20), "Macro creator") + (this.TextMacroAction := this.gViewer.Add("Text", "x900 y40 w40", "Action:")).SetFont("bold") + this.DDLMacroAction := this.gViewer.Add("DDL", "Choose1 x900 y38 w120", ["No element selected"]) + (this.ButMacroAddElement := this.gViewer.Add("Button","x900 y37 w90 h20", "&Add element")).SetFont("bold") + this.ButMacroAddElement.OnEvent("Click", this.ButMacroAddElement_Click.Bind(this)) + (this.EditMacroScript := this.gViewer.Add("Edit", "-Wrap HScroll x900 y65 h410 w" (this.MacroSidebarWidth-40), "#include UIA.ahk`n`n")).SetFont("s10") ; Setting a font here disables UTF-8-BOM + (this.ButMacroScriptRun := this.gViewer.Add("Button", "x880 y120 w70", "&Test script")).SetFont("bold") + this.ButMacroScriptRun.OnEvent("Click", this.ButMacroScriptRun_Click.Bind(this)) + this.ButMacroScriptCopy := this.gViewer.Add("Button", "x920 y120 w70", "&Copy") + this.ButMacroScriptCopy.OnEvent("Click", (*) => (A_Clipboard := this.EditMacroScript.Text, ToolTip("Macro code copied to Clipboard!"), SetTimer(ToolTip, -3000))) + this.ButToggleMacroSidebar := this.gViewer.Add("Button", "x490 y500 w120", "Show macro &sidebar =>") + this.ButToggleMacroSidebar.OnEvent("Click", this.ButToggleMacroSidebar_Click.Bind(this)) + xy := "" + if this.RememberGuiPosition { + xy := StrSplit(this.RememberGuiPosition, ","), monitor := 0 + Loop MonitorGetCount() { + MonitorGetWorkArea(A_Index, &Left, &Top, &Right, &Bottom) + if xy[1] > (Left-50) && xy[2] > (Top-50) && xy[1] < (Right-50) && xy[2] < (Bottom-30) { + monitor := A_Index + break + } + } + xy := monitor ? "x" xy[1] " y" xy[2] " " : "" + } + this.gViewer.Show(xy "w600 h550") + this.gViewer_Size(this.gViewer,0,600,550) + this.FocusHook := DllCall("SetWinEventHook", "UInt", 0x8005, "UInt", 0x8005, "Ptr",0,"Ptr", CallbackCreate(this.HandleFocusChangedEvent.Bind(this), "C", 7),"UInt", 0, "UInt",0, "UInt",0) + } + __Delete() { + DllCall("UnhookWinEvent", "Ptr", this.FocusHook) + } + HandleFocusChangedEvent(hWinEventHook, Event, hWnd, idObject, idChild, dwEventThread, dwmsEventTime) => SetTimer(this._HandleFocusChangedEvent.Bind(this, hWnd), -1) + _HandleFocusChangedEvent(hWnd) { + winHwnd := DllCall("GetAncestor", "Ptr", hWnd, "UInt", 2, "Ptr") + if winHwnd = this.gViewer.Hwnd { + if !this.Focused { + this.Focused := 1 + if IsObject(this.Stored.HighlightedElement) + this.Stored.HighlightedElement.Highlight(0, "Blue", 4) + } + } else { + if this.Focused { + this.Focused := 0 + if IsObject(this.Stored.HighlightedElement) + this.Stored.HighlightedElement.Highlight("clear") + } + } + if this.Capturing && this.Stored.HasOwnProp("CapturedWindowBuildUpdatedCache") + SetTimer(this.Stored.CapturedWindowBuildUpdatedCache, -2000) + return 0 + } + SaveSettings() { + if !FileExist(A_AppData "\UIAViewer") + DirCreate(A_AppData "\UIAViewer") + IniWrite(this.PathIgnoreNames, UIA.Viewer.SettingsFilePath, "Path", "IgnoreNames") + IniWrite(this.PathType, UIA.Viewer.SettingsFilePath, "Path", "Type") + IniWrite(this.AlwaysOnTop, UIA.Viewer.SettingsFilePath, "General", "AlwaysOnTop") + IniWrite(this.DPIAwareness, UIA.Viewer.SettingsFilePath, "General", "DPIAwareness") + IniWrite(this.RememberGuiPosition, UIA.Viewer.SettingsFilePath, "General", "RememberGuiPosition") + } + LoadSettings() { + this.PathIgnoreNames := IniRead(UIA.Viewer.SettingsFilePath, "Path", "IgnoreNames", 1) + this.PathType := IniRead(UIA.Viewer.SettingsFilePath, "Path", "Type", "") + this.AlwaysOnTop := IniRead(UIA.Viewer.SettingsFilePath, "General", "AlwaysOnTop", 1) + this.DPIAwareness := IniRead(UIA.Viewer.SettingsFilePath, "General", "DPIAwareness", 0) + this.RememberGuiPosition := IniRead(UIA.Viewer.SettingsFilePath, "General", "RememberGuiPosition", "") + } + ErrorHandler(Exception, Mode) => (OutputDebug(Format("{1} ({2}) : ({3}) {4}`n", Exception.File, Exception.Line, Exception.What, Exception.Message) (HasProp(Exception, "Extra") ? " Specifically: " Exception.Extra "`n" : "") "Stack:`n" Exception.Stack "`n`n"), 1) + gViewer_Close(GuiObj, *) { + local X, Y + if this.RememberGuiPosition { + WinGetPos(&X, &Y,,,GuiObj.Hwnd), this.RememberGuiPosition := X "," Y, this.SaveSettings() + } + ExitApp() + } + ; Resizes window controls when window is resized + gViewer_Size(GuiObj, MinMax, Width, Height) { + static RedrawFunc := WinRedraw.Bind(GuiObj.Hwnd) + this.TVUIA.GetPos(&TV_Pos_X, &TV_Pos_Y, &TV_Pos_W, &TV_Pos_H) + this.MoveControls(this.MacroSidebarVisible ? {Control:this.TVUIA,h:(TV_Pos_H:=Height-TV_Pos_Y-60)} : {Control:this.TVUIA,w:(TV_Pos_W:=Width-TV_Pos_X-10),h:(TV_Pos_H:=Height-TV_Pos_Y-60)}) + TV_Pos_R := TV_Pos_X+TV_Pos_W + this.LVProps.GetPos(&LVPropsX, &LVPropsY, &LVPropsWidth, &LVPropsHeight) + this.ButToggleMacroSidebar.GetPos(,,&ButToggleMacroSidebarW) + this.MoveControls( + {Control:this.TextFilterTVUIA, x:TV_Pos_X, y:Height-47}, + {Control:this.ButToggleMacroSidebar, x:TV_Pos_X+TV_Pos_W-ButToggleMacroSidebarW, y:Height-50}, + {Control:this.EditFilterTVUIA, x:TV_Pos_X+30, y:Height-50}, + {Control:this.LVProps,h:Height-LVPropsY-170}, + {Control:this.TextTVPatterns,y:Height-165}, + {Control:this.TVPatterns,y:Height-145}, + {Control:this.ButCapture,y:Height-50}) + if this.MacroSidebarVisible + this.MacroSidebarWidth := Width-(TV_Pos_X+TV_Pos_W)-10 + this.MoveControls( + {Control:this.GroupBoxMacro,x:TV_Pos_R+15, w:Width-TV_Pos_R-30, h:TV_Pos_H+35}, + {Control:this.TextMacroAction,x:TV_Pos_R+25}, + {Control:this.DDLMacroAction,x:TV_Pos_R+70}, + {Control:this.ButMacroAddElement,x:TV_Pos_R+this.MacroSidebarWidth-105}, + {Control:this.EditMacroScript,x:TV_Pos_R+25,w:Width-TV_Pos_R-50,h:TV_Pos_H-50}, + {Control:this.ButMacroScriptRun,x:TV_Pos_R+85,y:TV_Pos_Y+TV_Pos_H-2}, + {Control:this.ButMacroScriptCopy,x:TV_Pos_R+this.MacroSidebarWidth-145,y:TV_Pos_Y+TV_Pos_H-2}) + DllCall("RedrawWindow", "ptr", GuiObj.Hwnd, "ptr", 0, "ptr", 0, "uint", 0x0081) ; Reduces flicker compared to RedrawFunc + } + MoveControls(ctrls*) { + local ctrl + for ctrl in ctrls + ctrl.Control.Move(ctrl.HasOwnProp("x") ? ctrl.x : unset, ctrl.HasOwnProp("y") ? ctrl.y : unset, ctrl.HasOwnProp("w") ? ctrl.w : unset, ctrl.HasOwnProp("h") ? ctrl.h : unset) + } + ; Show/hide macros sidebar + ButToggleMacroSidebar_Click(GuiCtrlObj?, Info?) { + local w + this.MacroSidebarVisible := !this.MacroSidebarVisible + GuiCtrlObj.Text := this.MacroSidebarVisible ? "Hide macro &sidebar <=" : "Show macro &sidebar =>" + this.gViewer.GetPos(,, &w) + this.gViewer.Opt("+MinSize" (this.MacroSidebarVisible ? w + this.MacroSidebarMinWidth : this.GuiMinWidth) "x" this.GuiMinHeight) + this.gViewer.Move(,,w+(this.MacroSidebarVisible ? this.MacroSidebarWidth : -this.MacroSidebarWidth)) + } + ; Handles adding elements with actions to the macro Edit + ButMacroAddElement_Click(GuiCtrlObj?, Info?) { + local match, selectedElPath, winElVariable, winElText, processName + if !this.Stored.HasOwnProp("CapturedElement") + return + processName := WinGetProcessName(this.Stored.mwId) + winElVariable := RegExMatch(processName, "^[^ .\d]+", &match:="") ? RegExReplace(match[], "[^\p{L}0-9_#@$]") "El" : "winEl" ; Leaves only letters, numbers, and symbols _#@$ (allowed AHK characters) + winElText := winElVariable " := UIA.ElementFrom" (InStr(this.TextTVUIA.Text, "Chromium") ? "Chromium" : "Handle") "(`"" WinGetTitle(this.Stored.mwId) " ahk_exe " processName "`")" + if !InStr(this.EditMacroScript.Text, winElText) || RegExMatch(this.EditMacroScript.Text, "\Q" winElText "\E(?=[\w\W]*\QwinEl := UIA.ElementFromHandle(`"ahk_exe\E)") + this.EditMacroScript.Text := RTrim(this.EditMacroScript.Text, "`r`n`t ") "`r`n`r`n" winElText + else + this.EditMacroScript.Text := RTrim(this.EditMacroScript.Text, "`r`n`t ") + selectedElPath := SubStr(this.SBMain.Text, 9) + winElVariable := winElVariable (selectedElPath && StrLen(selectedElPath) > 2 ? ".ElementFromPath(" selectedElPath ")" : "") (this.DDLMacroAction.Text ? "." this.DDLMacroAction.Text : "") + if InStr(this.DDLMacroAction.Text, "Dump") + winElVariable := "MsgBox(" winElVariable ")" + this.EditMacroScript.Text := this.EditMacroScript.Text "`r`n" RegExReplace(winElVariable, "(? GuiCtrlObj.GetCount() + ? ListViewGetContent("", GuiCtrlObj) + : ListViewGetContent("Selected", GuiCtrlObj) + for LVData in StrSplit(LVData, "`n") { + LVData := StrSplit(LVData, "`t",,2) + if LVData.Length < 2 + continue + switch LVData[1], 0 { + case "Type": + LVData[2] := "`"" RTrim(SubStr(LVData[2],8), ")") "`"" + case "Location": + LVData[2] := "{" RegExReplace(LVData[2], "(\w:) (\d+)(?= )", "$1$2,") "}" + case "RuntimeId": + LVData[2] := "`"" LVData[2] "`"" + } + Property := -1 + try Property := UIA.Property.%LVData[1]% + out .= ", " (GuiCtrlObj.Hwnd = this.LVWin.Hwnd ? "" : LVData[1] ":") (UIA.PropertyVariantTypeBSTR.Has(Property) ? "`"" StrReplace(StrReplace(LVData[2], "``", "````"), "`"", "```"") "`"" : LVData[2]) + } + ToolTip("Copied: " (A_Clipboard := SubStr(out, 3))) + SetTimer(ToolTip, -3000) + } + ; Handles running pattern methods, first trying to find the live element by RuntimeId + TVPatterns_DoubleClick(GuiCtrlObj, Info) { + if !Info + return + Item := GuiCtrlObj.GetText(Info) + if !InStr(Item, "()") + return + Item := SubStr(Item, 1, -2) + if !WinExist(this.Stored.mwId) || !(WinEl := UIA.ElementFromHandle(this.Stored.mwId)) + return MsgBox("Target window not found!",,"4096") + if !(CurrentEl := WinEl.ElementExist({RuntimeId:this.Stored.CapturedElement.CachedRuntimeId}, 5)) + && !(CurrentEl := WinEl.ElementFromPathExist(Trim(this.Stored.CapturedElement.Path, "```""))) + return MsgBox("Live element not found!",,"4096") + if Item ~= "Value|Scroll(?!Into)" { + this.gViewer.Opt("-AlwaysOnTop") + Ret := InputBox("Insert value", Item, "W200 H120") + this.gViewer.Opt("+AlwaysOnTop") + if Ret.Result != "OK" + return + } + WinActivate(this.Stored.mwId) + WinWaitActive(this.Stored.mwId) + try CurrentEl.%GuiCtrlObj.GetText(GuiCtrlObj.GetParent(Info)) "Pattern"%.%Item%(IsSet(Ret) ? Ret.Value : unset) + } + ; Copies the UIA path to clipboard when statusbar is clicked + SBMain_Click(GuiCtrlObj, Info, *) { + if InStr(this.SBMain.Text, "Path:") { + ToolTip("Copied: " (A_Clipboard := SubStr(this.SBMain.Text, 9))) + SetTimer(ToolTip, -3000) + } + } + ; StatusBar context menu creation + SBMain_ContextMenu(GuiCtrlObj, Item, IsRightClick, X, Y) { + SBMain_Menu := Menu() + if InStr(this.SBMain.Text, "Path:") { + SBMain_Menu.Add("Copy UIA path", (*) => (ToolTip("Copied: " (A_Clipboard := this.Stored.CapturedElement.Path)), SetTimer(ToolTip, -3000))) + SBMain_Menu.Add("Copy condition path", (*) => (ToolTip("Copied: " (A_Clipboard := this.Stored.CapturedElement.ConditionPath)), SetTimer(ToolTip, -3000))) + SBMain_Menu.Add("Copy numeric path", (*) => (ToolTip("Copied: " (A_Clipboard := this.Stored.CapturedElement.NumericPath)), SetTimer(ToolTip, -3000))) + SBMain_Menu.Add() + } + SBMain_Menu.Add("Display UIA path (relatively reliable, shortest)", (*) => (this.PathType := this.PathType = "" ? "" : "", this.Stored.HasOwnProp("CapturedElement") && this.Stored.CapturedElement.HasOwnProp("Path") ? this.SBMain.SetText(" Path: " (this.PathType = "Numeric" ? this.Stored.CapturedElement.NumericPath : this.Stored.CapturedElement.Path)) : 1)) + SBMain_Menu.Add("Display numeric path (least reliable, short)", (*) => (this.PathType := this.PathType = "Numeric" ? "" : "Numeric", this.Stored.HasOwnProp("CapturedElement") && this.Stored.CapturedElement.HasOwnProp("Path") ? this.SBMain.SetText(" Path: " (this.PathType = "Numeric" ? this.Stored.CapturedElement.NumericPath : this.Stored.CapturedElement.Path)) : 1)) + SBMain_Menu.Add("Display condition path (most reliable, longest)", (*) => (this.PathType := this.PathType = "Condition" ? "" : "Condition", this.Stored.HasOwnProp("CapturedElement") && this.Stored.CapturedElement.HasOwnProp("Path") ? this.SBMain.SetText(" Path: " (this.PathType = "Condition" ? this.Stored.CapturedElement.ConditionPath : this.Stored.CapturedElement.Path)) : 1)) + SBMain_Menu.Add("Ignore Name properties in condition path", (*) => (this.PathIgnoreNames := !this.PathIgnoreNames)) + if this.PathIgnoreNames + SBMain_Menu.Check("Ignore Name properties in condition path") + if this.PathType = "" + SBMain_Menu.Check("Display UIA path (relatively reliable, shortest)") + if this.PathType = "Numeric" + SBMain_Menu.Check("Display numeric path (least reliable, short)") + if this.PathType = "Condition" + SBMain_Menu.Check("Display condition path (most reliable, longest)") + SBMain_Menu.Add() + SBMain_Menu.Add("UIAViewer always on top", (*) => (this.AlwaysOnTop := !this.AlwaysOnTop, this.gViewer.Opt((this.AlwaysOnTop ? "+" : "-") "AlwaysOnTop"))) + if this.AlwaysOnTop + SBMain_Menu.Check("UIAViewer always on top") + SBMain_Menu.Add("Remember UIAViewer position", (*) => (this.RememberGuiPosition := !this.RememberGuiPosition, this.SaveSettings())) + if this.RememberGuiPosition + SBMain_Menu.Check("Remember UIAViewer position") + SBMain_Menu.Add("Enable DPI awareness", (*) => (this.DPIAwareness := !this.DPIAwareness, this.DPIAwareness ? UIA.SetMaximumDPIAwareness() : UIA.DPIAwareness := -2)) + if this.DPIAwareness + SBMain_Menu.Check("Enable DPI awareness") + SBMain_Menu.Add() + SBMain_Menu.Add("Save settings", (*) => (this.SaveSettings(), ToolTip("Settings saved!"), SetTimer(ToolTip, -2000))) + SBMain_Menu.Show() + } + ; Stops capturing elements under mouse, unhooks CaptureCallback + StopCapture(GuiCtrlObj:=0, Info:=0) { + if this.Capturing { + this.Capturing := False + SetTimer(this.CaptureCallback, 0) + SetTimer(this.Stored.CapturedWindowBuildUpdatedCache, 0) + this.ButCapture.Text := "Start capturing (Alt+S)" + if IsObject(this.Stored.HighlightedElement) + this.Stored.HighlightedElement.Highlight("clear") + } + } + ; Gets UIA element under mouse, updates the GUI. + ; If the mouse is not moved for 1 second then constructs the UIA tree. + CaptureCycle() { + local clientX:=0, clientY:=0, clientW:=0, clientH:=0, mX, mY, mwId, mwCtrl, elBr, same, propsOrder + , mwX, mwY, mwW, mwH + SetTimer(this.CaptureCallback, -3000) ; In case of an error try again in 3 seconds + CoordMode "Mouse", "Screen" + MouseGetPos(&mX, &mY, &mwId := 0, &mwCtrl := 0, 2) + ; If is a Chromium window then also get the control, because some Chromium windows don't show content from main window + ; Also activate accessibility, afterwards don't activate anymore + try { + mwCtrl := ControlGetHwnd("Chrome_RenderWidgetHostHWND1", mwId) + UIA.ActivateChromiumAccessibility(mwId) + } + ; If the control was considered the window, assume that this is still needed + if mwCtrl && mwCtrl = this.Stored.mwId + mwId := mwCtrl + CapturedElement := this.TryUpdateElementVariables(mwId, mX, mY) + ; If ctrl and main window aren't the same, then compare their sizes. If sizes are the same + ; then instead try to get the element from the control under the mouse (eg Chromium content) + if mwCtrl && mwId != mwCtrl && this.Stored.mwId != mwCtrl && CapturedElement { + try WinGetClientPos(&clientX, &clientY, &clientW, &clientH, mwId) + elBr := CapturedElement.CachedBoundingRectangle, winBr := this.Stored.CapturedWindow.CachedBoundingRectangle + ; If element size is equal to window/client size, then probably it's not accessible through the window + ; Instead try to get the content through the control under the cursor + if Abs(elBr.l - winBr.l + elbr.t - winBr.t + elBr.r - winBr.r + elBr.b - winBr.b) < 4 + || Abs(elBr.l - clientX + elBr.t - clientY + elBr.r - (clientX+clientW) + elBr.b - (clientY+clientH)) < 4 { + CapturedElementCtrl := this.TryUpdateElementVariables(mwCtrl, mX, mY) + if CapturedElementCtrl { + clsNN := ControlGetClassNN(mwCtrl, mwId), CapturedElement := CapturedElementCtrl, mwId := mwCtrl + this.TextLVWin.Text := "Control Info for " (clsNN = "Chrome_RenderWidgetHostHWND1" ? "Chromium" : clsNN " Control") + this.TextTVUIA.Text := "UIA Tree for " (clsNN = "Chrome_RenderWidgetHostHWND1" ? "Chromium (" clsNN ")" : clsNN) + } + } + } + if this.Capturing + SetTimer(this.CaptureCallback, 200) + if !CapturedElement + return + if this.Stored.HasOwnProp("CapturedElement") && IsObject(CapturedElement) { + try same := UIA.CompareElementsEx(CapturedElement, this.Stored.CapturedElement) ; UIA.CompareElements(CapturedElement, this.Stored.CapturedElement) ; Cached element RuntimeIds sometimes cause an error here + if same ?? 0 { + if this.FoundTime != 0 && ((A_TickCount - this.FoundTime) > 500) { + if (mX == this.Stored.mX) && (mY == this.Stored.mY) { + if !(this.Stored.HasOwnProp("TreeViewWindow") && UIA.CompareElementsEx(this.Stored.CapturedWindow, this.Stored.TreeViewWindow)) + || !this.TreeViewSelectCapturedElement() + this.ConstructTreeView() + this.FoundTime := 0 + } else + this.FoundTime := A_TickCount + } + this.Stored.mX := mX, this.Stored.mY := mY + return + } + } + this.LVWin.Delete() + try WinGetPos(&mwX, &mwY, &mwW, &mwH, mwId) + catch + return + this.Stored.mwId := mwId, this.Stored.CapturedElement := CapturedElement, this.Stored.mX := mX, this.Stored.mY := mY, this.FoundTime := A_TickCount + propsOrder := ["Title", "Text", "Id", "Location", "Class(NN)", "Process", "PID"] + prop := Map("Title", WinGetTitle(mwId), "Text", WinGetText(mwId), "Id", mwId, "Location", "x: " mwX " y: " mwY " w: " mwW " h: " mwH, "Class(NN)", WinGetClass(mwId), "Process", WinGetProcessName(mwId), "PID", WinGetPID(mwId)) + for propName in propsOrder + this.LVWin.Add(,propName,prop[propName]) + this.PopulatePropsPatterns(CapturedElement) + } + + ; Updates this.Stored.CapturedWindowLive, this.Stored.CapturedWindow, this.Stored.CapturedWindowBuildUpdatedCache, returns CapturedElement + ; Also removes and adds back the StructureChangedEventHandler + TryUpdateElementVariables(mwId, mX, mY) { + if mwId != this.Stored.mwId { + if UIA.ProcessIsElevated(WinGetPID(mwId)) > 0 && !A_IsAdmin { + if MsgBox("The inspected window is running with elevated privileges.`nUIAViewer must be running in UIAccess mode or as administrator to inspect it.`n`nRun UIAViewer as administrator to inspect it?",, 0x1000 | 0x30 | 0x4) = "Yes" { + try { + Run('*RunAs "' (A_IsCompiled ? A_ScriptFullPath '" /restart' : A_AhkPath '" /restart "' A_ScriptFullPath '"')) + ExitApp + } + } + } + this.TextLVWin.Text := "Window Info", this.TextTVUIA.Text := "UIA Tree" + this.UpdateCapturedWindowCache(mwId) + } + try return CapturedElement := UIA.CachedSmallestElementInElementContainingPoint(mX, mY, this.Stored.CapturedWindow) + } + UpdateCapturedWindowCache(mwId?) { + SetTimer(this.Stored.CapturedWindowBuildUpdatedCache, 0) + if !WinExist(mwId ?? this.Stored.mwId) + return + this.Stored.CapturedWindowLive := UIA.ElementFromHandle(mwId ?? this.Stored.mwId,,false) + this.Stored.CapturedWindow := this.Stored.CapturedWindowLive.BuildUpdatedCache(this.CacheRequest) + } + ; Populates the listview with UIA element properties + PopulatePropsPatterns(Element) { + local v, value, pattern, parent, proto, match, X, Y, W, H, name, prop + if IsObject(this.Stored.HighlightedElement) + this.Stored.HighlightedElement.Highlight("clear") + this.Stored.HighlightedElement := Element + try { ; Show the Highlight only if the window is visible and + WinGetPos(&X, &Y, &W, &H, this.Stored.mwId) + if IsObject(this.Stored.HighlightedElement) && (elBR := this.Stored.HighlightedElement.CachedBoundingRectangle) && UIA.IntersectRect(X, Y, X+W, Y+H, elBR.l, elBR.t, elBR.r, elBR.b) + Element.Highlight(0, "Blue", 4) ; Indefinite show + } + this.LVProps.Delete() + this.TVPatterns.Delete() + for v in this.DisplayedProps { + try prop := Element.Cached%v% + switch v, 0 { + case "Type": + try name := UIA.Type[prop] + catch + name := "Unknown" + try this.LVProps.Add(, v, prop " (" name ")") + case "BoundingRectangle": + prop := prop ? prop : {l:0,t:0,r:0,b:0} + try this.LVProps.Add(, "Location", "x: " prop.l " y: " prop.t " w: " (prop.r - prop.l) " h: " (prop.b - prop.t)) + case "RuntimeId": + try this.LVProps.Add(, v, prop) + default: + try this.LVProps.Add(, v, prop) + } + prop := "" + } + lastAction := this.DDLMacroAction.Text + this.DDLMacroAction.Delete() + this.DDLMacroAction.Add(['', 'Click()', 'Click("left")', 'ControlClick()', 'SetFocus()', 'ShowContextMenu()', 'Highlight()', 'Dump()','DumpAll()']) + for pattern, value in UIA.Property.OwnProps() { + if RegExMatch(pattern, "Is([\w]+)Pattern(\d?)Available", &match:=0) && Element.GetCachedPropertyValue(value) { + parent := this.TVPatterns.Add(match[1] (match.Count > 1 ? match[2] : "")) + if !IsObject(UIA.IUIAutomation%match[1]%Pattern) + continue + proto := UIA.IUIAutomation%match[1]%Pattern.Prototype + switch match[1], 0 { + case "Invoke": + this.DDLMacroAction.Add(['Invoke()']) + case "ExpandCollapse": + this.DDLMacroAction.Add(['Expand()', 'Collapse()']) + case "Value": + this.DDLMacroAction.Add(['Value := "value"']) + case "Toggle": + this.DDLMacroAction.Add(['Toggle()']) + case "SelectionItem": + this.DDLMacroAction.Add(['Select()', 'AddToSelection()', 'RemoveFromSelection()']) + case "ScrollItem": + this.DDLMacroAction.Add(['ScrollIntoView()']) + } + for name in proto.OwnProps() { + if name ~= "i)^(_|Cached)" + continue + this.TVPatterns.Add(name (proto.GetOwnPropDesc(name).HasOwnProp("call") ? "()" : ""), parent) + } + } + } + try this.DDLMacroAction.Choose(lastAction) + catch + this.DDLMacroAction.Choose(7) + } + ; Handles selecting elements in the UIA tree, highlights the selected element + TVUIA_Click(GuiCtrlObj, Info) { + if this.Capturing + return + try Element := this.EditFilterTVUIA.Value ? this.Stored.FilteredTreeView[Info] : this.Stored.TreeView[Info] + if IsSet(Element) && Element { + if IsObject(this.Stored.HighlightedElement) { + if UIA.CompareElementsEx(Element, this.Stored.HighlightedElement) + return (this.Stored.HighlightedElement.Highlight("clear"), this.Stored.HighlightedElement := 0) + } + this.Stored.CapturedElement := Element + try this.SBMain.SetText(" Path: " (this.PathType = "Numeric" ? Element.NumericPath : this.PathType = "Condition" ? Element.ConditionPath : Element.Path)) + this.PopulatePropsPatterns(Element) + } + } + ; Permits copying the Dump of UIA element(s) to clipboard + TVUIA_ContextMenu(GuiCtrlObj, Item, IsRightClick, X, Y) { + this.TVUIA.Modify(Item, "Select") + this.TVUIA_Click(GuiCtrlObj, Item) + TVUIA_Menu := Menu() + try Element := this.EditFilterTVUIA.Value ? this.Stored.FilteredTreeView[Item] : this.Stored.TreeView[Item] + if IsSet(Element) + TVUIA_Menu.Add("Copy to Clipboard", (*) => (ToolTip("Copied Dump() output to Clipboard!"), A_Clipboard := Element.CachedDump(), SetTimer((*) => ToolTip(), -3000))) + TVUIA_Menu.Add("Copy Tree to Clipboard", (*) => (ToolTip("Copied DumpAll() output to Clipboard!"), A_Clipboard := UIA.ElementFromHandle(this.Stored.mwId, this.cacheRequest).DumpAll(), SetTimer((*) => ToolTip(), -3000))) + if IsSet(Element) { + TVUIA_Menu.Add("") + TVUIA_Menu.Add("Expand/Collapse All", this.TVUIA_ExpandCollapseAll.bind(this)) + TVUIA_Menu.Add("Focus Element", this.TVUIA_FocusElement.bind(this)) + } + TVUIA_Menu.Show() + } + TVUIA_ExpandCollapseAll(GuiCtrlObj, Item, *) { + FocusId := this.TVUIA.GetSelection() + expandCollapse := this.TVUIA.IsCollapsed ? "Expand" : "-Expand" + this.TVUIA.Opt("-Redraw") + ItemID := 0 + while ItemID := this.TVUIA.GetNext(ItemID, "Full") + this.TVUIA.Modify(ItemID, expandCollapse) + FocusId := this.TVUIA.isCollapsed ? FocusId : this.TVUIA.GetNext() + this.TVUIA.Modify(FocusId, "Select Vis") + this.TVUIA.Opt("+Redraw") + this.TVUIA.IsCollapsed := !this.TVUIA.IsCollapsed + } + TVUIA_FocusElement(GuiCtrlObj, *) { + FocusId := this.TVUIA.GetSelection() + ItemId := 0 + this.TVUIA.Opt("-Redraw") + while ItemID := this.TVUIA.GetNext(ItemID, "Full") ; Collapse all elements. + this.TVUIA.Modify(ItemID, "-Expand") + this.TVUIA.Modify(FocusId, "Select Vis") ; Reselect the item, which also expands the parents + this.TVUIA.Opt("+Redraw") + this.TVUIA.IsCollapsed := true + } + ; Handles filtering the UIA elements inside the TreeView when the text hasn't been changed in 500ms. + ; Sorts the results by UIA properties. + EditFilterTVUIA_Change(GuiCtrlObj, Info, *) { + static TimeoutFunc := "", ChangeActive := False + local index, prop + if !this.Stored.TreeView.Count + return + if (Info != "DoAction") || ChangeActive { + if !TimeoutFunc + TimeoutFunc := this.EditFilterTVUIA_Change.Bind(this, GuiCtrlObj, "DoAction") + SetTimer(TimeoutFunc, -500) + return + } + ChangeActive := True + this.Stored.FilteredTreeView := Map(), parents := Map() + if !(searchPhrase := this.EditFilterTVUIA.Value) { + this.ConstructTreeView() + ChangeActive := False + return + } + this.TVUIA.Delete() + temp := this.TVUIA.Add("Searching...") + Sleep -1 + this.TVUIA.Opt("-Redraw") + this.TVUIA.Delete() + for index, Element in this.Stored.TreeView { + for prop in this.DisplayedProps { + try { + if InStr(Element.Cached%Prop%, searchPhrase) { + if !parents.Has(prop) + parents[prop] := this.TVUIA.Add(prop,, "Expand") + this.Stored.FilteredTreeView[this.TVUIA.Add(this.GetShortDescription(Element), parents[prop], "Expand")] := Element + } + } + } + } + if !this.Stored.FilteredTreeView.Count + this.TVUIA.Add("No results found matching `"" searchPhrase "`"") + this.TVUIA.Opt("+Redraw") + TimeoutFunc := "", ChangeActive := False + } + ; Populates the TreeView with the UIA tree when capturing and the mouse is held still + ConstructTreeView() { + this.TVUIA.Delete() + this.TVUIA.Add("Constructing Tree, please wait...") + this.TVUIA.IsCollapsed := false + Sleep -1 + this.TVUIA.Opt("-Redraw") + this.TVUIA.Delete() + this.Stored.TreeView := Map() + this.Stored.TreeViewWindow := this.Stored.CapturedWindow + try this.RecurseTreeView(this.Stored.CapturedWindow) + catch { + this.Stored.TreeView := [] + this.TVUIA.Add("Error: unspecified error (window not found?)") + } + + this.TVUIA.Opt("+Redraw") + this.SBMain.SetText(" Path: ") + this.TreeViewSelectCapturedElement() + } + TreeViewSelectCapturedElement() { + local k, v + if !this.Stored.CapturedElement.HasOwnProp("Path") { + this.Stored.CapturedElement.DefineProp("Path", {Value:""}) + this.Stored.CapturedElement.DefineProp("NumericPath", {Value:""}) + this.Stored.CapturedElement.DefineProp("ConditionPath", {Value:""}) + } + for k, v in this.Stored.TreeView { + if UIA.CompareElementsEx(this.Stored.CapturedElement, v) { + this.TVUIA.Modify(k, "Vis Select"), this.SBMain.SetText(" Path: " (this.PathType = "Numeric" ? v.NumericPath : this.PathType = "Condition" ? v.ConditionPath : v.Path)) + , this.Stored.CapturedElement.Path := v.Path + , this.Stored.CapturedElement.NumericPath := v.NumericPath + , this.Stored.CapturedElement.ConditionPath := v.ConditionPath + return 1 + } + } + return 0 + } + ; Stores the UIA tree with corresponding path values for each element + RecurseTreeView(Element, parent:=0, path:="", conditionpath := "", numpath:="") { + local info, child, type, k, paths := Map(), childInfo := [], children := Element.CachedChildren + Element.DefineProp("Path", {value:"`"" path "`""}) + Element.DefineProp("ConditionPath", {value:conditionpath}) + Element.DefineProp("NumericPath", {value:numpath}) + this.Stored.TreeView[TWEl := this.TVUIA.Add(this.GetShortDescription(Element), parent, "Expand")] := Element + ; First count up all multiple-condition-index conditions and type-index conditions + ; This is to know whether the condition is the last of the matching ones, so we can use index -1 + ; This gives an important speed difference over regular indexing + for child in children { + compactCondition := this.GetCompactCondition(child, &paths, &typeCondition := "", &type:="") + childInfo.Push([compactCondition, paths[compactCondition], typeCondition, paths[typeCondition], type]) + } + ; Now create the final conditions and recurse the tree + for k, child in children { + info := childInfo[k], compactCondition := info[1], conditionIndex := info[2] + if conditionIndex > 1 && conditionIndex = paths[compactCondition] + conditionIndex := -1 + compactCondition .= conditionIndex = 1 ? "}" : ", i:" conditionIndex "}" + typeIndex := info[4] + if typeIndex > 1 && typeIndex = paths[info[3]] + typeIndex := -1 + this.RecurseTreeView(child, TWEl, path UIA.EncodePath([typeIndex = 1 ? {Type:info[5]} : {Type:info[5], i:typeIndex}]), conditionpath (conditionpath?", ":"") compactCondition, numpath (numpath?",":"") k) + } + } + ; Creates a short description string for the UIA tree elements + GetShortDescription(Element) { + local elDesc := " `"`"" + try elDesc := " `"" Element.CachedName "`"" + try elDesc := Element.CachedLocalizedType elDesc + catch + elDesc := "`"`"" elDesc + return elDesc + } + GetCompactCondition(Element, &pathsMap, &t := "", &type := "", &automationId := "", &className := "", &name := "") { + local n := "", c := "", a := "" + type := Element.CachedType + t := "{T:" (type-50000) + pathsMap[t] := pathsMap.Has(t) ? pathsMap[t] + 1 : 1 + try a := StrReplace(automationId := Element.CachedAutomationId, "`"", "```"") + if a != "" && !IsInteger(a) { ; Ignore Integer AutomationIds, since they seem to be auto-generated in Chromium apps + a := t ",A:`"" a "`"" + pathsMap[a] := pathsMap.Has(a) ? pathsMap[a] + 1 : 1 ; This actually shouldn't be needed, if AutomationId's are unique + } + try c := StrReplace(className := Element.CachedClassName, "`"", "```"") + if c != "" { + c := t ",CN:`"" c "`"" + pathsMap[c] := pathsMap.Has(c) ? pathsMap[c] + 1 : 1 + } + try n := StrReplace(name := Element.CachedName, "`"", "```"") + if !this.PathIgnoreNames && n != "" { ; Consider Name last, because it can change (eg. window title) + n := t ",N:`"" n "`"" + pathsMap[n] := pathsMap.Has(n) ? pathsMap[n] + 1 : 1 + } + if pathsMap[t] = 1 + return t + if a != "" && !IsInteger(a) { + return c != "" ? (pathsMap[a] <= pathsMap[c] ? a : c) : a + } else if c != "" + return c + else if !this.PathIgnoreNames && n != "" && (pathsMap[n] < pathsMap[t]) + return n + return t + } +} + +} diff --git a/src/lib/command_toast.ahk b/src/lib/command_toast.ahk index ed9fa3a..34ebaaa 100644 --- a/src/lib/command_toast.ahk +++ b/src/lib/command_toast.ahk @@ -378,6 +378,9 @@ BuildCommandToastRows(key_width := 16) { if Config.Has("window_selector") && Config["window_selector"]["enabled"] { rows.Push(Map("key", "super+" Config["window_selector"]["hotkey"], "desc", "window selector")) } + if Config.Has("screen_search") && Config["screen_search"]["enabled"] { + rows.Push(Map("key", "super+" Config["screen_search"]["hotkey"], "desc", "screen search")) + } if Config.Has("directional_focus") && Config["directional_focus"]["enabled"] { rows.Push(Map("key", "alt+h/l", "desc", "focus left/right")) rows.Push(Map("key", "alt+j/k", "desc", "focus down/up")) diff --git a/src/lib/config_loader.ahk b/src/lib/config_loader.ahk index 7df0da8..97490ff 100644 --- a/src/lib/config_loader.ahk +++ b/src/lib/config_loader.ahk @@ -102,6 +102,15 @@ ConfigSchema() { "include_minimized", "bool", "close_on_focus_loss", "bool" ), + "screen_search", Map( + "enabled", "bool", + "hotkey", "string", + "hint_chars", "string", + "max_results", "number", + "min_size_px", "number", + "min_distance_px", "number", + "hint_opacity", "number" + ), "window_manager", Map( "grid_size", "number", "margins", Map( diff --git a/src/lib/screen_search.ahk b/src/lib/screen_search.ahk new file mode 100644 index 0000000..5c3d960 --- /dev/null +++ b/src/lib/screen_search.ahk @@ -0,0 +1,632 @@ +global Config + +; Screen search (UI hints) using UI Automation. +class ScreenSearch +{ + static active := false + static gui := "" + static hints := Map() + static hint_labels := [] + static hint_elements := [] + static input_buffer := "" + static input_hook := 0 + static build_timer := 0 + static hide_timer := 0 + static focus_timer := 0 + static hidden := false + static target_hwnd := 0 + + static Toggle(*) { + if ScreenSearch.active { + ScreenSearch.Hide() + return + } + ScreenSearch.Show() + } + + static IsActive(*) => ScreenSearch.active + + static Show(*) { + global Config + if !Config.Has("screen_search") || !Config["screen_search"]["enabled"] + return + if ReloadModeActive() || Window.IsMoveMode() + return + hwnd := 0 + try hwnd := WinGetID("A") + if !hwnd + return + + ScreenSearch.active := true + ScreenSearch.target_hwnd := hwnd + ScreenSearch.input_buffer := "" + ScreenSearch.hidden := false + ScreenSearch.DestroyGui() + ScreenSearch.StartBuild() + ScreenSearch.StartInputHook() + ScreenSearch.StartFocusWatch() + } + + static Hide(*) { + ScreenSearch.active := false + ScreenSearch.hidden := false + ScreenSearch.target_hwnd := 0 + ScreenSearch.input_buffer := "" + ScreenSearch.StopBuild() + ScreenSearch.StopInputHook() + ScreenSearch.StopFocusWatch() + ScreenSearch.DestroyGui() + } + + static StartBuild() { + ScreenSearch.StopBuild() + ScreenSearch.build_timer := ObjBindMethod(ScreenSearch, "BuildHints") + SetTimer(ScreenSearch.build_timer, -10) + } + + static StopBuild() { + if ScreenSearch.build_timer + SetTimer(ScreenSearch.build_timer, 0) + ScreenSearch.build_timer := 0 + } + + static StartFocusWatch() { + if ScreenSearch.focus_timer + return + ScreenSearch.focus_timer := ObjBindMethod(ScreenSearch, "CheckFocus") + SetTimer(ScreenSearch.focus_timer, 120) + } + + static StopFocusWatch() { + if ScreenSearch.focus_timer + SetTimer(ScreenSearch.focus_timer, 0) + ScreenSearch.focus_timer := 0 + } + + static CheckFocus() { + if !ScreenSearch.active + return + hwnd := 0 + try hwnd := WinGetID("A") + if !hwnd || (ScreenSearch.target_hwnd && hwnd != ScreenSearch.target_hwnd) + ScreenSearch.Hide() + } + + static StartInputHook() { + ScreenSearch.StopInputHook() + ScreenSearch.input_hook := InputHook("L0") + ScreenSearch.input_hook.KeyOpt("{All}", "NS") + ScreenSearch.input_hook.OnKeyDown := ObjBindMethod(ScreenSearch, "OnKeyDown") + ScreenSearch.input_hook.Start() + } + + static StopInputHook() { + if ScreenSearch.input_hook { + try ScreenSearch.input_hook.Stop() + ScreenSearch.input_hook := 0 + } + ScreenSearch.StopHideTimer() + } + + static StopHideTimer() { + if ScreenSearch.hide_timer + SetTimer(ScreenSearch.hide_timer, 0) + ScreenSearch.hide_timer := 0 + } + + static OnKeyDown(hook, vk, sc) { + if !ScreenSearch.active + return + key_name := GetKeyName(Format("vk{:x}sc{:x}", vk, sc)) + if ScreenSearch.IsSuperKeyName(key_name) { + ScreenSearch.Hide() + return + } + if (key_name = "Esc" || key_name = "Escape") { + ScreenSearch.Hide() + return + } + if (key_name = "Space") { + ScreenSearch.HideHints() + return + } + if (key_name = "Backspace") { + if (StrLen(ScreenSearch.input_buffer) > 0) + ScreenSearch.input_buffer := SubStr(ScreenSearch.input_buffer, 1, -1) + ScreenSearch.UpdateHintVisibility() + return + } + + if (StrLen(key_name) != 1) + return + + input_char := StrLower(key_name) + if !ScreenSearch.IsHintChar(input_char) + return + ScreenSearch.input_buffer .= input_char + ScreenSearch.MatchInput() + } + + static IsSuperKeyName(key_name) { + global Config + if !key_name + return false + super_keys := Config["super_key"] + if !(super_keys is Array) + super_keys := [super_keys] + key_lower := StrLower(key_name) + for _, key in super_keys { + if (StrLower(key) = key_lower) + return true + } + return false + } + + static IsHintChar(char) { + global Config + hint_chars := Config["screen_search"]["hint_chars"] + if !hint_chars + hint_chars := "asdfghjklqwertyuiopzxcvbnm" + return InStr(hint_chars, char) > 0 + } + + static HideHints() { + if !ScreenSearch.active || ScreenSearch.hidden + return + ScreenSearch.hidden := true + if ScreenSearch.gui + ScreenSearch.gui.Hide() + ScreenSearch.hide_timer := ObjBindMethod(ScreenSearch, "CheckSpaceRelease") + SetTimer(ScreenSearch.hide_timer, 30) + } + + static CheckSpaceRelease() { + if !ScreenSearch.active { + ScreenSearch.StopHideTimer() + return + } + if GetKeyState("Space", "P") + return + ScreenSearch.StopHideTimer() + ScreenSearch.ShowHints() + } + + static ShowHints() { + if !ScreenSearch.active || !ScreenSearch.hidden + return + ScreenSearch.hidden := false + if ScreenSearch.gui + ScreenSearch.gui.Show("NoActivate") + } + + static MatchInput() { + if !ScreenSearch.active + return + input := ScreenSearch.input_buffer + if (input = "") + { + ScreenSearch.UpdateHintVisibility() + return + } + ScreenSearch.UpdateHintVisibility() + if !ScreenSearch.hints.Has(input) { + has_prefix := false + for label in ScreenSearch.hints { + if InStr(label, input) = 1 { + has_prefix := true + break + } + } + if !has_prefix + ScreenSearch.input_buffer := "" + return + } + hint := ScreenSearch.hints[input] + ScreenSearch.ActivateHint(hint) + } + + static UpdateHintVisibility() { + if !ScreenSearch.gui + return + input := ScreenSearch.input_buffer + for label, hint in ScreenSearch.hints { + ctrl := hint["control"] + if !ctrl + continue + if (input = "") { + ctrl.Visible := true + continue + } + ctrl.Visible := (InStr(label, input) = 1) + } + } + + static ActivateHint(hint) { + ScreenSearch.Hide() + if !(hint is Map) + return + if !hint.Has("element") + return + element := hint["element"] + if !element + return + if ScreenSearch.TryInvokeElement(element) + return + if ScreenSearch.TrySelectionElement(element) + return + if ScreenSearch.TryToggleElement(element) + return + if ScreenSearch.TryExpandCollapseElement(element) + return + ScreenSearch.TryClickElement(element) + } + + static TryInvokeElement(element) { + try { + if element.GetCachedPropertyValue(UIA.Property.IsInvokePatternAvailable) { + element.InvokePattern.Invoke() + return true + } + } + return false + } + + static TrySelectionElement(element) { + try { + if element.GetCachedPropertyValue(UIA.Property.IsSelectionItemPatternAvailable) { + element.SelectionItemPattern.Select() + return true + } + } + return false + } + + static TryToggleElement(element) { + try { + if element.GetCachedPropertyValue(UIA.Property.IsTogglePatternAvailable) { + element.TogglePattern.Toggle() + return true + } + } + return false + } + + static TryExpandCollapseElement(element) { + try { + if element.GetCachedPropertyValue(UIA.Property.IsExpandCollapsePatternAvailable) { + element.ExpandCollapsePattern.Expand() + return true + } + } + try { + if element.GetCachedPropertyValue(UIA.Property.IsExpandCollapsePatternAvailable) { + element.ExpandCollapsePattern.Collapse() + return true + } + } + return false + } + + static TryClickElement(element) { + try { + element.Click() + return true + } + return false + } + + static BuildHints(*) { + if !ScreenSearch.active + return + if !ScreenSearch.target_hwnd || !WinExist("ahk_id " ScreenSearch.target_hwnd) { + ScreenSearch.Hide() + return + } + elements := ScreenSearch.CollectElements(ScreenSearch.target_hwnd) + if (elements.Length = 0) { + ScreenSearch.Hide() + return + } + ScreenSearch.RenderHints(elements) + } + + static CollectElements(hwnd) { + global Config + results := [] + hint_config := Config["screen_search"] + max_results := hint_config["max_results"] + min_size := hint_config["min_size_px"] + min_distance := hint_config["min_distance_px"] + + cache_request := UIA.CreateCacheRequest([ + "BoundingRectangle", + "IsOffscreen", + "IsEnabled", + "IsKeyboardFocusable", + "IsInvokePatternAvailable", + "IsExpandCollapsePatternAvailable", + "IsTogglePatternAvailable", + "IsSelectionItemPatternAvailable", + "ControlType", + "Name" + ]) + + centers := [] + hwnds := ScreenSearch.GetThreadWindows(hwnd) + for _, target_hwnd in hwnds { + window_rect := ScreenSearch.GetWindowRect(target_hwnd) + if !window_rect + continue + elements := [] + try { + root := UIA.ElementFromHandle(target_hwnd) + elements := root.FindAllBuildCache(cache_request, UIA.TrueCondition, 4) + } catch { + continue + } + + for _, element in elements { + rect := "" + try rect := element.CachedBoundingRectangle + catch + continue + if !rect + continue + width := rect.r - rect.l + height := rect.b - rect.t + if (width <= 0 || height <= 0) + continue + if (width < min_size && height < min_size) + continue + maxed_out := results.Length >= max_results + + is_offscreen := 0 + is_enabled := 0 + try is_offscreen := element.GetCachedPropertyValue(UIA.Property.IsOffscreen) + catch + continue + try is_enabled := element.GetCachedPropertyValue(UIA.Property.IsEnabled) + catch + continue + if is_offscreen + continue + if !is_enabled + continue + + is_focusable := 0 + is_invoke := 0 + is_expand := 0 + is_toggle := 0 + is_select := 0 + control_type := 0 + try is_focusable := element.GetCachedPropertyValue(UIA.Property.IsKeyboardFocusable) + try is_invoke := element.GetCachedPropertyValue(UIA.Property.IsInvokePatternAvailable) + try is_expand := element.GetCachedPropertyValue(UIA.Property.IsExpandCollapsePatternAvailable) + try is_toggle := element.GetCachedPropertyValue(UIA.Property.IsTogglePatternAvailable) + try is_select := element.GetCachedPropertyValue(UIA.Property.IsSelectionItemPatternAvailable) + try control_type := element.GetCachedPropertyValue(UIA.Property.ControlType) + if !(is_focusable || is_invoke || is_expand || is_toggle || is_select || control_type = UIA.Type.Button) + continue + + center_x := rect.l + (width / 2) + center_y := rect.t + (height / 2) + if (center_x < window_rect["left"] || center_x > window_rect["right"] + || center_y < window_rect["top"] || center_y > window_rect["bottom"]) + continue + + adaptive_min_distance := Round(height * 0.6) + if (adaptive_min_distance < 1) + adaptive_min_distance := 1 + if (adaptive_min_distance > min_distance) + adaptive_min_distance := min_distance + + area := width * height + replace_index := 0 + best_replace_area := 0 + too_close := false + for center_index, center in centers { + dx := center["x"] - center_x + dy := center["y"] - center_y + min_spacing := adaptive_min_distance + if (center["min_distance"] < min_spacing) + min_spacing := center["min_distance"] + if (dx * dx + dy * dy < min_spacing * min_spacing) { + too_close := true + if (area < center["area"] && center["area"] > best_replace_area) { + best_replace_area := center["area"] + replace_index := center_index + } + } + } + if too_close { + if (replace_index > 0) { + result_index := centers[replace_index]["result_index"] + centers[replace_index] := Map( + "x", center_x, + "y", center_y, + "area", area, + "min_distance", adaptive_min_distance, + "result_index", result_index + ) + results[result_index] := Map( + "element", element, + "x", center_x, + "y", center_y + ) + } + continue + } + + if maxed_out + continue + + centers.Push(Map( + "x", center_x, + "y", center_y, + "area", area, + "min_distance", adaptive_min_distance, + "result_index", results.Length + 1 + )) + results.Push(Map( + "element", element, + "x", center_x, + "y", center_y + )) + } + } + + return results + } + + static GetThreadWindows(hwnd) { + hwnds := [hwnd] + if !hwnd + return hwnds + thread_id := DllCall("GetWindowThreadProcessId", "ptr", hwnd, "uint*", &pid := 0, "uint") + if !thread_id + return hwnds + + callback := CallbackCreate(__EnumThreadWindows, "Fast", 2) + DllCall("EnumThreadWindows", "uint", thread_id, "ptr", callback, "ptr", 0) + CallbackFree(callback) + return hwnds + + __EnumThreadWindows(window_hwnd, lparam) { + if (window_hwnd = hwnd) + return true + if !DllCall("IsWindowVisible", "ptr", window_hwnd) + return true + if !IsWindowOnCurrentDesktop(window_hwnd) + return true + if ScreenSearch.IsWindowCloaked(window_hwnd) + return true + hwnds.Push(window_hwnd) + return true + } + } + + static IsWindowCloaked(hwnd) { + cloaked := 0 + if !DllCall("dwmapi\DwmGetWindowAttribute", "ptr", hwnd, "int", 14, "int*", &cloaked, "int", 4) + return false + return cloaked != 0 + } + + static GetWindowRect(hwnd) { + rect := Buffer(16, 0) + if !DllCall("GetWindowRect", "ptr", hwnd, "ptr", rect) + return "" + left := NumGet(rect, 0, "int") + top := NumGet(rect, 4, "int") + right := NumGet(rect, 8, "int") + bottom := NumGet(rect, 12, "int") + return Map("left", left, "top", top, "right", right, "bottom", bottom) + } + + static RenderHints(elements) { + ScreenSearch.DestroyGui() + ScreenSearch.hints := Map() + ScreenSearch.hint_labels := [] + ScreenSearch.hint_elements := elements + if (elements.Length = 0) + return + + hint_chars := ScreenSearch.GetHintChars() + label_length := ScreenSearch.ComputeLabelLength(elements.Length, hint_chars.Length) + + ScreenSearch.gui := Gui("+AlwaysOnTop -Caption +ToolWindow +E0x20 +E0x80000", "harken Screen Search") + ScreenSearch.gui.BackColor := "FF00FF" + ScreenSearch.gui.SetFont("s10 w700", "Segoe UI") + ScreenSearch.gui.MarginX := 0 + ScreenSearch.gui.MarginY := 0 + + dpi_scale := A_ScreenDPI / 96 + padding := Round(4 * dpi_scale) + if (padding < 3) + padding := 3 + bg_color := "F3C623" + text_color := "101010" + + virtual_left := SysGet(76) + virtual_top := SysGet(77) + virtual_width := SysGet(78) + virtual_height := SysGet(79) + + for index, hint in elements { + label := ScreenSearch.IndexToLabel(index - 1, label_length, hint_chars) + label_display := StrUpper(label) + label_key := StrLower(label) + x := Round(hint["x"]) + y := Round(hint["y"]) + text_w := (StrLen(label_display) * 8) + (padding * 2) + text_h := Round(18 * dpi_scale) + pos_x := x - (text_w // 2) - virtual_left + pos_y := y - (text_h // 2) - virtual_top + + ctrl := ScreenSearch.gui.AddText( + "x" pos_x " y" pos_y " w" text_w " h" text_h " Center Background" bg_color " c" text_color, + label_display + ) + ScreenSearch.hints[label_key] := Map( + "element", hint["element"], + "control", ctrl + ) + ScreenSearch.hint_labels.Push(label_key) + } + + ScreenSearch.gui.Show("Hide x" virtual_left " y" virtual_top " w" virtual_width " h" virtual_height) + ScreenSearch.ApplyHintOpacity() + ScreenSearch.gui.Show("NoActivate x" virtual_left " y" virtual_top " w" virtual_width " h" virtual_height) + } + + static ApplyHintOpacity() { + global Config + opacity := 255 + if Config.Has("screen_search") && Config["screen_search"].Has("hint_opacity") + opacity := Config["screen_search"]["hint_opacity"] + if !IsNumber(opacity) + return + if (opacity < 40) + opacity := 40 + if (opacity > 255) + opacity := 255 + color_key := 0x00FF00FF + DllCall("SetLayeredWindowAttributes", "ptr", ScreenSearch.gui.Hwnd, "uint", color_key, "uchar", opacity, "uint", 0x3) + } + + static DestroyGui() { + if ScreenSearch.gui { + try ScreenSearch.gui.Destroy() + } + ScreenSearch.gui := "" + } + + static GetHintChars() { + global Config + hint_chars := Config["screen_search"]["hint_chars"] + if !hint_chars + hint_chars := "asdfghjklqwertyuiopzxcvbnm" + chars := [] + loop parse, hint_chars + chars.Push(A_LoopField) + return chars + } + + static ComputeLabelLength(count, base) { + length := 1 + while (base ** length < count) + length += 1 + return length + } + + static IndexToLabel(index, length, chars) { + base := chars.Length + label := "" + loop length { + digit := Mod(index, base) + label := chars[digit + 1] . label + index := Floor(index / base) + } + return label + } +} From 7b8595dd87fe68ffadbad457261269ff83c9579c Mon Sep 17 00:00:00 2001 From: Grant Kilber Date: Tue, 3 Mar 2026 00:45:25 -0600 Subject: [PATCH 18/20] better hints with windows explorer; avoid hidden dll elements er sumtin --- src/lib/screen_search.ahk | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/lib/screen_search.ahk b/src/lib/screen_search.ahk index 5c3d960..8e55329 100644 --- a/src/lib/screen_search.ahk +++ b/src/lib/screen_search.ahk @@ -234,10 +234,10 @@ class ScreenSearch if !ctrl continue if (input = "") { - ctrl.Visible := true + try ctrl.Visible := true continue } - ctrl.Visible := (InStr(label, input) = 1) + try ctrl.Visible := (InStr(label, input) = 1) } } @@ -405,13 +405,14 @@ class ScreenSearch try is_toggle := element.GetCachedPropertyValue(UIA.Property.IsTogglePatternAvailable) try is_select := element.GetCachedPropertyValue(UIA.Property.IsSelectionItemPatternAvailable) try control_type := element.GetCachedPropertyValue(UIA.Property.ControlType) - if !(is_focusable || is_invoke || is_expand || is_toggle || is_select || control_type = UIA.Type.Button) + if !(is_focusable || is_invoke || is_expand || is_toggle || is_select || control_type = UIA.Type.Button) { continue + } center_x := rect.l + (width / 2) center_y := rect.t + (height / 2) - if (center_x < window_rect["left"] || center_x > window_rect["right"] - || center_y < window_rect["top"] || center_y > window_rect["bottom"]) + if (rect.l < window_rect["left"] || rect.r > window_rect["right"] + || rect.t < window_rect["top"] || rect.b > window_rect["bottom"]) continue adaptive_min_distance := Round(height * 0.6) @@ -523,6 +524,7 @@ class ScreenSearch return Map("left", left, "top", top, "right", right, "bottom", bottom) } + static RenderHints(elements) { ScreenSearch.DestroyGui() ScreenSearch.hints := Map() From 7445e73b797fd10e49dd7087839e140b5d4b296f Mon Sep 17 00:00:00 2001 From: Grant Kilber Date: Tue, 3 Mar 2026 10:29:03 -0600 Subject: [PATCH 19/20] =?UTF-8?q?-=20double=E2=80=91super=20opens=20Task?= =?UTF-8?q?=20View,=20and=20h/j/k/l=20navigate=20while=20it=E2=80=99s=20op?= =?UTF-8?q?en,=20spacebar=20can=20select=20-=20super+wheel=20switches=20de?= =?UTF-8?q?sktops;=20super+alt+wheel=20no=20longer=20does?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- harken.ahk | 1 + src/hotkeys/window.ahk | 74 +++++++++++++++++++++++++++++++-------- src/lib/command_toast.ahk | 4 +++ 3 files changed, 64 insertions(+), 15 deletions(-) diff --git a/harken.ahk b/harken.ahk index 6a8bf3c..f023531 100644 --- a/harken.ahk +++ b/harken.ahk @@ -51,6 +51,7 @@ Hotkey("r", (*) => ExecuteCommand(() => Reload())) Hotkey("i", (*) => ExecuteCommand(OpenWindowInspector)) Hotkey("n", (*) => ExecuteCommand(ToggleCommandHelper)) Hotkey("w", (*) => ExecuteCommand(OpenNewWindowForActiveApp)) +Hotkey("m", (*) => ExecuteCommand(EnterMoveMode)) Hotkey("g", (*) => ExecuteCommand(ReapplyDesktopAssignments)) Hotkey("e", (*) => ExecuteCommand(OpenConfigFile)) Hotkey("Esc", ClearReloadMode) diff --git a/src/hotkeys/window.ahk b/src/hotkeys/window.ahk index d5074bf..428be90 100644 --- a/src/hotkeys/window.ahk +++ b/src/hotkeys/window.ahk @@ -516,7 +516,8 @@ HandleSuperTap() { } if (A_TickCount - last_super_tap <= super_double_tap_ms) { - Window.SetMoveMode(true) + ; Use native overview instead of move mode (heuristic detects Task View state). + Send("#{Tab}") UpdateCommandToastVisibility() last_super_tap := 0 return @@ -526,9 +527,7 @@ HandleSuperTap() { } OnSuperKeyUp() { - global move_mode_enabled - if move_mode_enabled - HandleSuperTap() + HandleSuperTap() if ReloadModeActive() { global reload_mode_activated_at if (A_TickCount - reload_mode_activated_at < 500) { @@ -884,17 +883,6 @@ if (vd_next_hotkey != "") LogVirtualDesktopAction("goto_relative hotkey=" vd_next_hotkey " delta=1 current=" GetCurrentDesktopNumFresh()), GoToRelativeDesktop(1) )) -if (vd_scroll_switch) { - ; Optional wheel navigation to avoid breaking existing super+wheel resize behavior by default. - Hotkey("*WheelUp", (*) => ( - LogVirtualDesktopAction("goto_relative hotkey=*WheelUp delta=-1 current=" GetCurrentDesktopNumFresh()), - GoToRelativeDesktop(-1) - )) - Hotkey("*WheelDown", (*) => ( - LogVirtualDesktopAction("goto_relative hotkey=*WheelDown delta=1 current=" GetCurrentDesktopNumFresh()), - GoToRelativeDesktop(1) - )) -} LogVirtualDesktopHotkeys("prev_hotkey=" vd_prev_hotkey " next_hotkey=" vd_next_hotkey) for _, entry in vd_goto_hotkeys { if !(entry is Map) @@ -931,6 +919,20 @@ if (vd_desktop_hotkeys is Array && vd_desktop_hotkeys.Length > 0) { } HotIf +HotIf (*) => IsSuperKeyPressed() && !GetKeyState("Shift", "P") +if (vd_scroll_switch) { + ; Use super+wheel for desktop switching to match the native overview flow. + Hotkey("*WheelUp", (*) => ( + LogVirtualDesktopAction("goto_relative hotkey=*WheelUp delta=-1 current=" GetCurrentDesktopNumFresh()), + GoToRelativeDesktop(-1) + )) + Hotkey("*WheelDown", (*) => ( + LogVirtualDesktopAction("goto_relative hotkey=*WheelDown delta=1 current=" GetCurrentDesktopNumFresh()), + GoToRelativeDesktop(1) + )) +} +HotIf + HotIf (*) => IsSuperKeyPressed() && IsAltPressed() && GetKeyState("Shift", "P") if (vd_move_prev_hotkey != "") Hotkey(vd_move_prev_hotkey, (*) => ( @@ -1000,11 +1002,53 @@ if move_mode_enabled { HotIf } +HotIf IsOverviewActive +Hotkey("h", (*) => Send("{Left}")) +Hotkey("l", (*) => Send("{Right}")) +Hotkey("j", (*) => Send("{Down}")) +Hotkey("k", (*) => Send("{Up}")) +HotIf + +EnterMoveMode(*) { + global move_mode_enabled + if !move_mode_enabled + return + Window.SetMoveMode(true) + UpdateCommandToastVisibility() +} + ExitMoveMode() { Window.SetMoveMode(false) UpdateCommandToastVisibility() } +IsOverviewActive(*) { + ; Heuristic: Task View classes vary by Windows build, so allow a broader list. + static overview_classes := ["MultitaskingViewFrame", "TaskViewFrame", "XamlExplorerHostIslandWindow"] + static allowed_processes := Map("explorer.exe", true, "ShellExperienceHost.exe", true) + + for _, class_name in overview_classes { + hwnds := WinGetList("ahk_class " class_name) + for _, hwnd in hwnds { + if !hwnd + continue + process_name := "" + try process_name := WinGetProcessName("ahk_id " hwnd) + process_lower := StrLower(process_name) + if (process_lower = "" || !allowed_processes.Has(process_lower)) + continue + if (class_name = "XamlExplorerHostIslandWindow") { + title := "" + try title := WinGetTitle("ahk_id " hwnd) + if (title != "" && !InStr(title, "Task View") && !InStr(title, "Multitasking")) + continue + } + return true + } + } + return false +} + BeginSuperDrag(*) { if !IsSuperKeyPressed() return diff --git a/src/lib/command_toast.ahk b/src/lib/command_toast.ahk index 34ebaaa..c0b0c41 100644 --- a/src/lib/command_toast.ahk +++ b/src/lib/command_toast.ahk @@ -345,6 +345,8 @@ BuildCommandToastModel() { lines.Push(FormatRow("i", "window inspector", key_width)) lines.Push(FormatRow("n", "toggle command overlay", key_width)) lines.Push(FormatRow("w", "new window (active app)", key_width)) + if Config["window"]["move_mode"]["enable"] + lines.Push(FormatRow("m", "enter move mode", key_width)) lines.Push(FormatRow("g", "reapply app desktops", key_width)) lines.Push(FormatRow("Esc/super", "exit command mode", key_width)) model["mode"] := "command" @@ -370,6 +372,8 @@ BuildCommandToastRows(key_width := 16) { rows.Push(Map("key", "super+shift+h/j/k/l", "desc", "resize center")) rows.Push(Map("key", "super+ctrl+h/j/k/l", "desc", "move")) rows.Push(Map("key", "super+m", "desc", "maximize")) + rows.Push(Map("key", "double-super", "desc", "native overview")) + rows.Push(Map("key", "overview h/j/k/l", "desc", "navigate")) rows.Push(Map("key", "alt+-", "desc", "minimize")) rows.Push(Map("key", "alt+q", "desc", "close")) rows.Push(Map("key", "super+" Config["window"]["cycle_app_windows_hotkey"], "desc", "cycle app windows")) From 6da7e1fa2c62cc3d232001a8c24220dd8c2801b5 Mon Sep 17 00:00:00 2001 From: Grant Kilber Date: Thu, 5 Mar 2026 10:38:49 -0600 Subject: [PATCH 20/20] =?UTF-8?q?Improve=20desktop=20switching=20UX,=20scr?= =?UTF-8?q?een=20search=20hints,=20and=20docs=20while=20formalizing=20a=20?= =?UTF-8?q?human=E2=80=91curated=20changelog.=20Add=20an=20optional=20swit?= =?UTF-8?q?ch=20curtain=20to=20mask=20Task=20View=20flicker,=20update=20vi?= =?UTF-8?q?rtual=20desktop=20hotkeys/config,=20and=20document=20new=20beha?= =?UTF-8?q?viors=20in=20README/CHANGELOG.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 3 + CHANGELOG.md | 54 ++++++ README.md | 5 +- config/config.example.toml | 9 +- harken.ahk | 8 +- src/hotkeys/window.ahk | 36 +++- src/lib/config_loader.ahk | 8 +- src/lib/focus_or_run.ahk | 9 +- src/lib/screen_search.ahk | 363 +++++++++++++++++++++++++++++++++++- src/lib/virtual_desktop.ahk | 134 ++++++++++++- src/lib/window_manager.ahk | 6 +- 11 files changed, 606 insertions(+), 29 deletions(-) create mode 100644 CHANGELOG.md diff --git a/AGENTS.md b/AGENTS.md index ee75257..f6c4aec 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -108,6 +108,9 @@ Comment the "why" in and around code to help provide context from the instructio - Avoid breaking changes without a migration path. - Keep new features optional and discoverable. - Keep `README.md` and `AGENTS.md` aligned with current behavior. +- Maintain `CHANGELOG.md` for user-facing changes: + - Add entries under `Unreleased` as part of feature/behavior work. + - Move entries into a versioned section when cutting a release. ## Suggested Manual Checks - Launch `harken.ahk` with a clean `harken.toml` and verify hotkeys. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ddca95d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,54 @@ +# Changelog + +All notable changes to this project are documented here. This changelog is human curated. + +## [Unreleased] +### Added +- Screen Search (`super + .`) with clickable hints (like vimium/surfingkeys/homerow but jankier). +- UI Automation support via `src/lib/UIA.ahk` with licensing documentation. +- Command mode action to send windows to their assigned desktops. +- App matching enhancements: process-tree targeting and `ignore_classes` support. +- `docs/COMPILE.md` with build and packaging notes. +- Virtual desktop scroll navigation and move-window scroll support (`super+scroll` & `super+alt+shift+scroll`). +- Optional desktop switch curtain to mask brief background flashes. +- Project now uses CHANGELOG.md + +### Changed +- Double-super now opens native Task View; move mode is entered from command mode, and h/j/k/l navigate Task View. +- Super+scroll switches virtual desktops (replacing the prior super+alt+scroll combo). +- Config flow favors `config_watch` instead of a dedicated reload section. +- Example config updates (apps list and focus border thickness). +- App matching now uses cached window metadata and PID-based lookups for off-desktop windows, with focus diagnostics logging. +- README adds a comparison section with similar tools. +- Config validation favors warnings over hard errors where possible. + +### Fixed +- Window walker rendering issues when the resolution changes. +- Window cycling now includes more than two tiles reliably. +- Grid snapping edge cases. +- EnumWindows callback signature. + +## [0.2.1] - 2026-02-07 +### Added +- New entry point `harken.ahk` and TOML configuration support with a TOML example config. +- Virtual desktop integration via `VD.ahk` with licensing documentation. + +### Changed +- Project rename to Harken and migration away from JSON config. +- Install, release, and build tooling updated for the new name/config. +- README refreshed to reflect the new app name and setup steps. + +## [0.2.0] - 2026-02-03 +### Added +- Directional focus hotkeys and supporting window focus helpers. +- Window switcher (window walker) UI and hotkeys. +- State store for persisted app state. +- Additional documentation assets for the overlay and switcher UI. + +### Changed +- README updates with new features and sources. +- Config example refreshed for the new hotkeys. + +## [0.0.1] - 2026-01-28 +### Added +- Initial public release of the original script. diff --git a/README.md b/README.md index b5f968e..a2e4a59 100644 --- a/README.md +++ b/README.md @@ -176,13 +176,14 @@ Add `debug = true` under `match.process_tree` to log process tree details to | Shortcut | Action | | --- | --- | | `super + alt + h/l` | Previous/next desktop | -| `super + alt + WheelUp/WheelDown` | Previous/next desktop (when `virtual_desktop.scroll_switch = true`) | +| `super + WheelUp/WheelDown` | Previous/next desktop (when `virtual_desktop.scroll_switch = true`) | | `super + alt + shift + h/l` | Move window to previous/next desktop (follow) | | `super + alt + shift + WheelUp/WheelDown` | Move window to previous/next desktop (when `virtual_desktop.scroll_switch = true`) | | `super + alt + ` | Go to mapped desktop (`[[virtual_desktop.]]`) | | `super + alt + shift + ` | Move window to mapped desktop (follow) | | `virtual_desktop.auto_assign` | Move newly created windows that match `apps[]` with `desktop` set | | `virtual_desktop.debug_focus` | Log cross-desktop focus attempts to `%APPDATA%\harken\vd.focus.debug.log` | +| `virtual_desktop.switch_curtain` | Dim overlay during desktop switches to reduce flicker | #### Apps (defaults) @@ -205,6 +206,7 @@ These are examples for the launch-or-focus keybindings. | `w` | Open a new window for the active app | | `n` | Toggle command overlay | | `i` | Open window inspector | +| `m` | Enter move mode | | `Esc` | Exit command mode | @@ -235,6 +237,7 @@ max_results = 200 min_size_px = 12 min_distance_px = 40 hint_opacity = 235 +debug_log = false ``` ## Limitations diff --git a/config/config.example.toml b/config/config.example.toml index 2746552..d7af86b 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -164,6 +164,7 @@ max_results = 200 min_size_px = 12 min_distance_px = 40 hint_opacity = 235 +debug_log = false # ------------------------------------------------------------- # Window manager (grid + gap) @@ -190,10 +191,16 @@ switch_on_focus = true ensure_count = 0 cycle_prefer_current = true -# Optional mouse wheel switching (super+alt+wheel). +# Optional mouse wheel switching (super+wheel). # Keep disabled if you use super+wheel for resize elsewhere. scroll_switch = true +[virtual_desktop.switch_curtain] +enabled = true +# 0-255 opacity (204 ~= 80%). +opacity = 204 +color = "#202020" + # Hotkey fields here use AHK syntax without super (super is implied). # Modifiers are applied by Harken (alt or alt+shift) for desktop actions. prev_hotkey = "h" diff --git a/harken.ahk b/harken.ahk index f023531..a32bdae 100644 --- a/harken.ahk +++ b/harken.ahk @@ -122,7 +122,8 @@ DefaultConfig() { "max_results", 200, "min_size_px", 12, "min_distance_px", 40, - "hint_opacity", 235 + "hint_opacity", 235, + "debug_log", false ), "config_watch", Map( "enabled", false, @@ -145,6 +146,11 @@ DefaultConfig() { "ensure_count", 0, "cycle_prefer_current", true, "scroll_switch", false, + "switch_curtain", Map( + "enabled", true, + "opacity", 204, + "color", "#202020" + ), "prev_hotkey", "h", "next_hotkey", "l", "move_prev_hotkey", "h", diff --git a/src/hotkeys/window.ahk b/src/hotkeys/window.ahk index 428be90..414808f 100644 --- a/src/hotkeys/window.ahk +++ b/src/hotkeys/window.ahk @@ -798,8 +798,13 @@ GoToRelativeDesktop(delta) { target := VD.modulusResolveDesktopNum(current + delta) LogVirtualDesktopAction("goto_relative current=" current " delta=" delta " target=" target) RefreshVirtualDesktopState() - VD.goToDesktopNum(target) - VD.WaitDesktopSwitched(target) + curtain_visible := BeginDesktopSwitchCurtain() + try { + VD.goToDesktopNum(target) + VD.WaitDesktopSwitched(target) + } finally { + EndDesktopSwitchCurtain(curtain_visible) + } RefreshVirtualDesktopState() } @@ -811,8 +816,13 @@ GoToDesktopNumber(desktop_num) { LogVirtualDesktopAction("goto_absolute target=" desktop_num " current=" GetCurrentDesktopNumFresh()) RefreshVirtualDesktopState() GetCurrentDesktopNumFresh() - VD.goToDesktopNum(desktop_num) - VD.WaitDesktopSwitched(desktop_num) + curtain_visible := BeginDesktopSwitchCurtain() + try { + VD.goToDesktopNum(desktop_num) + VD.WaitDesktopSwitched(desktop_num) + } finally { + EndDesktopSwitchCurtain(curtain_visible) + } RefreshVirtualDesktopState() } @@ -828,8 +838,13 @@ MoveWindowToRelativeDesktop(delta) { target := VD.modulusResolveDesktopNum(current + delta) LogVirtualDesktopAction("move_relative current=" current " delta=" delta " target=" target) RefreshVirtualDesktopState() - VD.MoveWindowToDesktopNum("A", target, true) - VD.WaitDesktopSwitched(target) + curtain_visible := BeginDesktopSwitchCurtain() + try { + VD.MoveWindowToDesktopNum("A", target, true) + VD.WaitDesktopSwitched(target) + } finally { + EndDesktopSwitchCurtain(curtain_visible) + } RefreshVirtualDesktopState() } @@ -841,8 +856,13 @@ MoveWindowToDesktopNumber(desktop_num) { LogVirtualDesktopAction("move_absolute target=" desktop_num " current=" GetCurrentDesktopNumFresh()) RefreshVirtualDesktopState() GetCurrentDesktopNumFresh() - VD.MoveWindowToDesktopNum("A", desktop_num, true) - VD.WaitDesktopSwitched(desktop_num) + curtain_visible := BeginDesktopSwitchCurtain() + try { + VD.MoveWindowToDesktopNum("A", desktop_num, true) + VD.WaitDesktopSwitched(desktop_num) + } finally { + EndDesktopSwitchCurtain(curtain_visible) + } RefreshVirtualDesktopState() } diff --git a/src/lib/config_loader.ahk b/src/lib/config_loader.ahk index 97490ff..aecf6e3 100644 --- a/src/lib/config_loader.ahk +++ b/src/lib/config_loader.ahk @@ -109,7 +109,8 @@ ConfigSchema() { "max_results", "number", "min_size_px", "number", "min_distance_px", "number", - "hint_opacity", "number" + "hint_opacity", "number", + "debug_log", "bool" ), "window_manager", Map( "grid_size", "number", @@ -128,6 +129,11 @@ ConfigSchema() { "ensure_count", "number", "cycle_prefer_current", "bool", "scroll_switch", "bool", + "switch_curtain", Map( + "enabled", "bool", + "opacity", "number", + "color", "string" + ), "prev_hotkey", "string", "next_hotkey", "string", "move_prev_hotkey", "string", diff --git a/src/lib/focus_or_run.ahk b/src/lib/focus_or_run.ahk index dbc5b06..166e538 100644 --- a/src/lib/focus_or_run.ahk +++ b/src/lib/focus_or_run.ahk @@ -587,8 +587,13 @@ TryMoveAppWindowToDesktop(win_title, app_config, target_desktop, follow_on_spawn if (hwnd) { VD.MoveWindowToDesktopNum("ahk_id " hwnd, target_desktop, follow_on_spawn) if follow_on_spawn { - VD.goToDesktopNum(target_desktop) - VD.WaitDesktopSwitched(target_desktop) + curtain_visible := BeginDesktopSwitchCurtain() + try { + VD.goToDesktopNum(target_desktop) + VD.WaitDesktopSwitched(target_desktop) + } finally { + EndDesktopSwitchCurtain(curtain_visible) + } } try { assigned_desktop := GetWindowDesktopNum(hwnd) diff --git a/src/lib/screen_search.ahk b/src/lib/screen_search.ahk index 8e55329..88e9be8 100644 --- a/src/lib/screen_search.ahk +++ b/src/lib/screen_search.ahk @@ -15,6 +15,7 @@ class ScreenSearch static focus_timer := 0 static hidden := false static target_hwnd := 0 + static explorer_active := false static Toggle(*) { if ScreenSearch.active { @@ -39,8 +40,10 @@ class ScreenSearch ScreenSearch.active := true ScreenSearch.target_hwnd := hwnd + ScreenSearch.explorer_active := ScreenSearch.IsExplorerWindow(hwnd) ScreenSearch.input_buffer := "" ScreenSearch.hidden := false + ScreenSearch.ResetDebugLog() ScreenSearch.DestroyGui() ScreenSearch.StartBuild() ScreenSearch.StartInputHook() @@ -250,6 +253,7 @@ class ScreenSearch element := hint["element"] if !element return + control_type := hint.Has("control_type") ? hint["control_type"] : 0 if ScreenSearch.TryInvokeElement(element) return if ScreenSearch.TrySelectionElement(element) @@ -258,6 +262,14 @@ class ScreenSearch return if ScreenSearch.TryExpandCollapseElement(element) return + if ScreenSearch.explorer_active && ScreenSearch.IsListControlType(control_type) { + if ScreenSearch.TryExplorerEnterElement(element) + return + } + if ScreenSearch.explorer_active && ScreenSearch.IsListControlType(control_type) { + if ScreenSearch.TryDoubleClickElement(element) + return + } ScreenSearch.TryClickElement(element) } @@ -315,6 +327,23 @@ class ScreenSearch return false } + static TryDoubleClickElement(element) { + try { + element.Click("left", 2) + return true + } + return false + } + + static TryExplorerEnterElement(element) { + try { + element.SetFocus() + Send "{Enter}" + return true + } + return false + } + static BuildHints(*) { if !ScreenSearch.active return @@ -405,15 +434,23 @@ class ScreenSearch try is_toggle := element.GetCachedPropertyValue(UIA.Property.IsTogglePatternAvailable) try is_select := element.GetCachedPropertyValue(UIA.Property.IsSelectionItemPatternAvailable) try control_type := element.GetCachedPropertyValue(UIA.Property.ControlType) + name := "" + try name := element.GetCachedPropertyValue(UIA.Property.Name) if !(is_focusable || is_invoke || is_expand || is_toggle || is_select || control_type = UIA.Type.Button) { continue } + actionable := is_invoke || is_select || is_toggle || is_expand center_x := rect.l + (width / 2) center_y := rect.t + (height / 2) - if (rect.l < window_rect["left"] || rect.r > window_rect["right"] - || rect.t < window_rect["top"] || rect.b > window_rect["bottom"]) + if actionable { + if (center_x < window_rect["left"] || center_x > window_rect["right"] + || center_y < window_rect["top"] || center_y > window_rect["bottom"]) + continue + } else if (rect.l < window_rect["left"] || rect.r > window_rect["right"] + || rect.t < window_rect["top"] || rect.b > window_rect["bottom"]) { continue + } adaptive_min_distance := Round(height * 0.6) if (adaptive_min_distance < 1) @@ -433,9 +470,15 @@ class ScreenSearch min_spacing := center["min_distance"] if (dx * dx + dy * dy < min_spacing * min_spacing) { too_close := true - if (area < center["area"] && center["area"] > best_replace_area) { - best_replace_area := center["area"] + if (actionable && !center["actionable"]) { replace_index := center_index + break + } + if (actionable = center["actionable"]) { + if (area < center["area"] && center["area"] > best_replace_area) { + best_replace_area := center["area"] + replace_index := center_index + } } } } @@ -446,13 +489,25 @@ class ScreenSearch "x", center_x, "y", center_y, "area", area, + "actionable", actionable, "min_distance", adaptive_min_distance, "result_index", result_index ) results[result_index] := Map( "element", element, "x", center_x, - "y", center_y + "y", center_y, + "area", area, + "actionable", actionable, + "control_type", control_type, + "height", height, + "width", width, + "focusable", is_focusable, + "invoke", is_invoke, + "expand", is_expand, + "toggle", is_toggle, + "select", is_select, + "name", name ) } continue @@ -465,13 +520,25 @@ class ScreenSearch "x", center_x, "y", center_y, "area", area, + "actionable", actionable, "min_distance", adaptive_min_distance, "result_index", results.Length + 1 )) results.Push(Map( "element", element, "x", center_x, - "y", center_y + "y", center_y, + "area", area, + "actionable", actionable, + "control_type", control_type, + "height", height, + "width", width, + "focusable", is_focusable, + "invoke", is_invoke, + "expand", is_expand, + "toggle", is_toggle, + "select", is_select, + "name", name )) } } @@ -524,11 +591,20 @@ class ScreenSearch return Map("left", left, "top", top, "right", right, "bottom", bottom) } + static IsExplorerWindow(hwnd) { + class_name := "" + try class_name := WinGetClass("ahk_id " hwnd) + return class_name = "CabinetWClass" || class_name = "ExploreWClass" + } + static RenderHints(elements) { ScreenSearch.DestroyGui() ScreenSearch.hints := Map() ScreenSearch.hint_labels := [] + if ScreenSearch.explorer_active + elements := ScreenSearch.CollapseExplorerRows(elements) + elements := ScreenSearch.SortHints(elements) ScreenSearch.hint_elements := elements if (elements.Length = 0) return @@ -554,6 +630,7 @@ class ScreenSearch virtual_width := SysGet(78) virtual_height := SysGet(79) + placed_rects := [] for index, hint in elements { label := ScreenSearch.IndexToLabel(index - 1, label_length, hint_chars) label_display := StrUpper(label) @@ -564,21 +641,224 @@ class ScreenSearch text_h := Round(18 * dpi_scale) pos_x := x - (text_w // 2) - virtual_left pos_y := y - (text_h // 2) - virtual_top + pos_x := ScreenSearch.Clamp(pos_x, 0, virtual_width - text_w) + pos_y := ScreenSearch.Clamp(pos_y, 0, virtual_height - text_h) + + nudged := ScreenSearch.NudgeHintRect(pos_x, pos_y, text_w, text_h, placed_rects, + virtual_width, virtual_height) + pos_x := nudged["x"] + pos_y := nudged["y"] + placed_rects.Push(Map("x", pos_x, "y", pos_y, "w", text_w, "h", text_h)) ctrl := ScreenSearch.gui.AddText( "x" pos_x " y" pos_y " w" text_w " h" text_h " Center Background" bg_color " c" text_color, label_display ) - ScreenSearch.hints[label_key] := Map( - "element", hint["element"], - "control", ctrl - ) + ScreenSearch.hints[label_key] := Map( + "element", hint["element"], + "control", ctrl, + "control_type", hint.Has("control_type") ? hint["control_type"] : 0 + ) ScreenSearch.hint_labels.Push(label_key) } ScreenSearch.gui.Show("Hide x" virtual_left " y" virtual_top " w" virtual_width " h" virtual_height) ScreenSearch.ApplyHintOpacity() ScreenSearch.gui.Show("NoActivate x" virtual_left " y" virtual_top " w" virtual_width " h" virtual_height) + ScreenSearch.ApplyHintOpacity() + } + + static SortHints(elements) { + try elements.Sort((a, b) => ScreenSearch.CompareHints(a, b)) + return elements + } + + static CompareHints(a, b) { + a_actionable := a.Has("actionable") ? a["actionable"] : false + b_actionable := b.Has("actionable") ? b["actionable"] : false + if (a_actionable != b_actionable) + return a_actionable ? -1 : 1 + a_area := a.Has("area") ? a["area"] : 0 + b_area := b.Has("area") ? b["area"] : 0 + if (a_area != b_area) + return a_area < b_area ? -1 : 1 + if (a["y"] != b["y"]) + return a["y"] < b["y"] ? -1 : 1 + if (a["x"] != b["x"]) + return a["x"] < b["x"] ? -1 : 1 + return 0 + } + + static CollapseExplorerRows(elements) { + if (elements.Length <= 1) + return elements + rows := [] + filtered := [] + list_items := [] + for _, element in elements { + control_type := element.Has("control_type") ? element["control_type"] : 0 + if ScreenSearch.IsExplorerRowControlType(control_type) + list_items.Push(element) + } + + if (list_items.Length > 0) { + try list_items.Sort((a, b) => ScreenSearch.CompareRowPosition(a, b)) + } + + for _, element in elements { + control_type := element.Has("control_type") ? element["control_type"] : 0 + is_list_item := ScreenSearch.IsExplorerRowControlType(control_type) + height := element.Has("height") ? element["height"] : 0 + + if !is_list_item { + ; Avoid duplicate hint rows by skipping non-list elements that sit on list items. + if ScreenSearch.IsInListRow(element, list_items) + continue + filtered.Push(element) + continue + } + + matched_row := 0 + for row_index, row in rows { + threshold := Max(row["height"], height) * 0.6 + if (threshold < 10) + threshold := 10 + if (Abs(row["y"] - element["y"]) <= threshold) { + matched_row := row_index + break + } + } + + if (matched_row = 0) { + rows.Push(Map( + "y", element["y"], + "height", height, + "best", element + )) + continue + } + + row := rows[matched_row] + if ScreenSearch.IsBetterExplorerRowCandidate(element, row["best"]) { + row["best"] := element + } + } + + for _, row in rows { + filtered.Push(row["best"]) + } + + if ScreenSearch.ShouldDebugLog() { + ScreenSearch.LogExplorerRows(rows) + } + + return filtered + } + + static IsInListRow(element, list_items) { + if (list_items.Length = 0) + return false + for _, list_item in list_items { + y_threshold := Max(list_item["height"], element.Has("height") ? element["height"] : 0) * 0.6 + if (y_threshold < 10) + y_threshold := 10 + x_threshold := Max(list_item.Has("width") ? list_item["width"] : 0, + element.Has("width") ? element["width"] : 0) * 0.6 + if (x_threshold < 40) + x_threshold := 40 + if (Abs(list_item["y"] - element["y"]) <= y_threshold + && Abs(list_item["x"] - element["x"]) <= x_threshold) + return true + } + return false + } + + static IsListControlType(control_type) { + return control_type = UIA.Type.ListItem || control_type = UIA.Type.TreeItem || control_type = UIA.Type.DataItem + } + + static IsExplorerRowControlType(control_type) { + return control_type = UIA.Type.ListItem || control_type = UIA.Type.DataItem + } + + static CompareRowPosition(a, b) { + if (a["y"] != b["y"]) + return a["y"] < b["y"] ? -1 : 1 + if (a["x"] != b["x"]) + return a["x"] < b["x"] ? -1 : 1 + return 0 + } + + static IsBetterExplorerRowCandidate(candidate, current) { + if !current + return true + candidate_rank := ScreenSearch.PatternRank(candidate) + current_rank := ScreenSearch.PatternRank(current) + if (candidate_rank != current_rank) + return candidate_rank > current_rank + candidate_area := candidate.Has("area") ? candidate["area"] : 0 + current_area := current.Has("area") ? current["area"] : 0 + if (candidate_area != current_area) + return candidate_area < current_area + return false + } + + static PatternRank(element) { + if element.Has("select") && element["select"] + return 4 + if element.Has("invoke") && element["invoke"] + return 3 + if (element.Has("toggle") && element["toggle"]) || (element.Has("expand") && element["expand"]) + return 2 + if element.Has("focusable") && element["focusable"] + return 1 + return 0 + } + + static ShouldDebugLog() { + global Config + return Config.Has("screen_search") && Config["screen_search"]["debug_log"] + } + + static ResetDebugLog() { + if !ScreenSearch.ShouldDebugLog() + return + log_path := ScreenSearch.DebugLogPath() + try FileDelete(log_path) + } + + static DebugLogPath() { + return A_AppData "\harken\screen_search.debug.log" + } + + static LogExplorerRows(rows) { + if !ScreenSearch.ShouldDebugLog() + return + log_path := ScreenSearch.DebugLogPath() + output := "Explorer rows: " rows.Length + for _, row in rows { + best := row["best"] + output .= "`n" ScreenSearch.DescribeElement(best) + } + FileAppend(output "`n", log_path) + } + + static DescribeElement(element) { + name := element.Has("name") ? element["name"] : "" + control_type := element.Has("control_type") ? element["control_type"] : 0 + line := "row y=" element["y"] " x=" element["x"] " type=" control_type " name=" name + line .= " select=" ScreenSearch.BoolStr(element, "select") + line .= " invoke=" ScreenSearch.BoolStr(element, "invoke") + line .= " toggle=" ScreenSearch.BoolStr(element, "toggle") + line .= " expand=" ScreenSearch.BoolStr(element, "expand") + line .= " focusable=" ScreenSearch.BoolStr(element, "focusable") + return line + } + + static BoolStr(element, key) { + if element.Has(key) && element[key] + return "1" + return "0" } static ApplyHintOpacity() { @@ -614,6 +894,69 @@ class ScreenSearch return chars } + static Clamp(value, min_value, max_value) { + if (value < min_value) + return min_value + if (value > max_value) + return max_value + return value + } + + static NudgeHintRect(x, y, w, h, placed_rects, bounds_w, bounds_h) { + if (placed_rects.Length = 0) + return Map("x", x, "y", y) + + if !ScreenSearch.RectIntersectsAny(x, y, w, h, placed_rects) + return Map("x", x, "y", y) + + step := 8 + max_rings := 6 + offsets := [ + [0, -1], + [-1, 0], + [-1, -1], + [1, 0], + [0, 1], + [1, 1], + [1, -1], + [-1, 1] + ] + + loop max_rings { + radius := A_Index * step + for _, dir in offsets { + candidate_x := x + (dir[1] * radius) + candidate_y := y + (dir[2] * radius) + candidate_x := ScreenSearch.Clamp(candidate_x, 0, bounds_w - w) + candidate_y := ScreenSearch.Clamp(candidate_y, 0, bounds_h - h) + if !ScreenSearch.RectIntersectsAny(candidate_x, candidate_y, w, h, placed_rects) + return Map("x", candidate_x, "y", candidate_y) + } + } + + return Map("x", x, "y", y) + } + + static RectIntersectsAny(x, y, w, h, placed_rects) { + for _, rect in placed_rects { + if ScreenSearch.RectsOverlap(x, y, w, h, rect["x"], rect["y"], rect["w"], rect["h"]) + return true + } + return false + } + + static RectsOverlap(x1, y1, w1, h1, x2, y2, w2, h2) { + if (x1 + w1 <= x2) + return false + if (x2 + w2 <= x1) + return false + if (y1 + h1 <= y2) + return false + if (y2 + h2 <= y1) + return false + return true + } + static ComputeLabelLength(count, base) { length := 1 while (base ** length < count) diff --git a/src/lib/virtual_desktop.ahk b/src/lib/virtual_desktop.ahk index 8818a41..6f4e1a9 100644 --- a/src/lib/virtual_desktop.ahk +++ b/src/lib/virtual_desktop.ahk @@ -10,6 +10,118 @@ VirtualDesktopEnabled() { return Config["virtual_desktop"]["enabled"] } +SwitchCurtainEnabled() { + global Config + if !VirtualDesktopEnabled() + return false + if !Config["virtual_desktop"].Has("switch_curtain") + return false + curtain := Config["virtual_desktop"]["switch_curtain"] + if !(curtain is Map) + return false + if !curtain.Has("enabled") + return false + return curtain["enabled"] +} + +SwitchCurtainOpacity() { + global Config + opacity := 204 + if Config["virtual_desktop"].Has("switch_curtain") { + curtain := Config["virtual_desktop"]["switch_curtain"] + if (curtain is Map && curtain.Has("opacity")) + opacity := curtain["opacity"] + } + if !(opacity is Number) + opacity := 204 + return Max(0, Min(255, Round(opacity))) +} + +SwitchCurtainColor() { + global Config + color := "#202020" + if Config["virtual_desktop"].Has("switch_curtain") { + curtain := Config["virtual_desktop"]["switch_curtain"] + if (curtain is Map && curtain.Has("color")) + color := curtain["color"] + } + return NormalizeHexColor(color, "202020") +} + +NormalizeHexColor(value, fallback) { + if !(value is String) + return fallback + trimmed := Trim(value) + if (SubStr(trimmed, 1, 1) = "#") + trimmed := SubStr(trimmed, 2) + if (StrLen(trimmed) = 8 && RegExMatch(trimmed, "i)^0x[0-9a-f]{6}$")) + return SubStr(trimmed, 3) + if (StrLen(trimmed) = 6 && RegExMatch(trimmed, "i)^[0-9a-f]{6}$")) + return trimmed + return fallback +} + +GetVirtualScreenBounds(&left, &top, &width, &height) { + try { + left := SysGet(76) + top := SysGet(77) + width := SysGet(78) + height := SysGet(79) + } catch { + left := 0 + top := 0 + width := A_ScreenWidth + height := A_ScreenHeight + } + if (width <= 0 || height <= 0) { + left := 0 + top := 0 + width := A_ScreenWidth + height := A_ScreenHeight + } +} + +global switch_curtain_gui := "" +global switch_curtain_visible := false + +ShowSwitchCurtain() { + global switch_curtain_gui, switch_curtain_visible + if !SwitchCurtainEnabled() + return false + if !switch_curtain_gui { + switch_curtain_gui := Gui("+AlwaysOnTop -Caption +ToolWindow +E0x20", "harken Desktop Switch") + switch_curtain_gui.MarginX := 0 + switch_curtain_gui.MarginY := 0 + switch_curtain_gui.BackColor := SwitchCurtainColor() + } + GetVirtualScreenBounds(&left, &top, &width, &height) + switch_curtain_gui.BackColor := SwitchCurtainColor() + switch_curtain_gui.Show("NoActivate x" left " y" top " w" width " h" height) + WinSetTransparent(SwitchCurtainOpacity(), switch_curtain_gui) + switch_curtain_visible := true + return true +} + +HideSwitchCurtain() { + global switch_curtain_gui, switch_curtain_visible + if !switch_curtain_gui + return + switch_curtain_gui.Hide() + switch_curtain_visible := false +} + +BeginDesktopSwitchCurtain() { + if !SwitchCurtainEnabled() + return false + return ShowSwitchCurtain() +} + +EndDesktopSwitchCurtain(was_shown := false) { + if !was_shown + return + HideSwitchCurtain() +} + global vd_auto_assign_timer := 0 VirtualDesktopTrayEnabled() { @@ -147,8 +259,13 @@ TryAutoAssignWindow(hwnd) { follow_on_spawn := app["follow_on_spawn"] VD.MoveWindowToDesktopNum("ahk_id " hwnd, target_desktop, follow_on_spawn) if follow_on_spawn { - VD.goToDesktopNum(target_desktop) - VD.WaitDesktopSwitched(target_desktop) + curtain_visible := BeginDesktopSwitchCurtain() + try { + VD.goToDesktopNum(target_desktop) + VD.WaitDesktopSwitched(target_desktop) + } finally { + EndDesktopSwitchCurtain(curtain_visible) + } } return } @@ -456,6 +573,7 @@ ActivateWindowAcrossDesktops(hwnd) { desktop_num := GetWindowDesktopNum(hwnd) current_desktop := GetCurrentDesktopNumFresh() if (desktop_num > 0 && desktop_num != current_desktop) { + curtain_visible := BeginDesktopSwitchCurtain() try { VD.goToDesktopOfWindow("ahk_id " hwnd, true) } catch as err { @@ -465,9 +583,12 @@ ActivateWindowAcrossDesktops(hwnd) { LogDesktopFocusDebug("switch_fallback", hwnd, desktop_num) VD.goToDesktopNum(desktop_num) VD.WaitDesktopSwitched(desktop_num) - } catch + } catch { LogDesktopFocusDebug("switch_failed", hwnd, desktop_num) return 0 + } + } finally { + EndDesktopSwitchCurtain(curtain_visible) } try { if VirtualDesktopFocusDebugEnabled() @@ -483,7 +604,12 @@ ActivateWindowAcrossDesktops(hwnd) { try { if VirtualDesktopFocusDebugEnabled() LogDesktopFocusDebug("switch_unknown", hwnd, desktop_num) - VD.goToDesktopOfWindow("ahk_id " hwnd, true) + curtain_visible := BeginDesktopSwitchCurtain() + try { + VD.goToDesktopOfWindow("ahk_id " hwnd, true) + } finally { + EndDesktopSwitchCurtain(curtain_visible) + } try { WinActivate "ahk_id " hwnd return WinGetID("A") diff --git a/src/lib/window_manager.ahk b/src/lib/window_manager.ahk index 66ca3ef..309ad5b 100644 --- a/src/lib/window_manager.ahk +++ b/src/lib/window_manager.ahk @@ -578,7 +578,11 @@ class Gui_Guides { if !this.gui return - if !WinExist("ahk_id " this.gui.Hwnd) + hwnd := 0 + try hwnd := this.gui.Hwnd + catch + return + if !hwnd || !WinExist("ahk_id " hwnd) return this.gui.GetPos(,, &width, &height) ; get position of gui WinSetRegion('0-0 w' width ' h' height ' r' ; use position to round the corners