|
4 | 4 | #include <wrl/client.h> |
5 | 5 |
|
6 | 6 | #include <algorithm> |
| 7 | +#include <functional> |
7 | 8 | #include <initializer_list> |
8 | 9 | #include <optional> |
9 | 10 | #include <string_view> |
| 11 | +#include <vector> |
10 | 12 |
|
11 | 13 | #include "utils.h" |
12 | 14 |
|
@@ -69,6 +71,9 @@ UiaThreadState& GetUiaThreadState() { |
69 | 71 | struct TabContainer { |
70 | 72 | ComPtr<IUIAutomationElement> container; |
71 | 73 | std::wstring_view tab_class; |
| 74 | + bool found_via_raw = false; |
| 75 | + std::optional<int> raw_tab_count; |
| 76 | + int raw_tab_visited = 0; |
72 | 77 | }; |
73 | 78 |
|
74 | 79 | bool EnsureAutomation() { |
@@ -208,6 +213,216 @@ ComPtr<IUIAutomationElement> FindFirstInSubtree( |
208 | 213 | return hit; |
209 | 214 | } |
210 | 215 |
|
| 216 | +std::optional<int> CountClassInSubtreeRaw( |
| 217 | + const ComPtr<IUIAutomation>& automation, |
| 218 | + const ComPtr<IUIAutomationElement>& root, |
| 219 | + std::wstring_view class_name, |
| 220 | + int visit_limit, |
| 221 | + int* visited_out = nullptr) { |
| 222 | + if (!automation || !root || class_name.empty() || visit_limit <= 0) |
| 223 | + return std::nullopt; |
| 224 | + |
| 225 | + ComPtr<IUIAutomationTreeWalker> walker; |
| 226 | + if (FAILED(automation->get_RawViewWalker(&walker)) || !walker) |
| 227 | + return std::nullopt; |
| 228 | + |
| 229 | + int count = 0; |
| 230 | + int visited = 0; |
| 231 | + std::function<void(IUIAutomationElement*)> dfs; |
| 232 | + dfs = [&](IUIAutomationElement* node) { |
| 233 | + if (!node || visited >= visit_limit) |
| 234 | + return; |
| 235 | + |
| 236 | + ++visited; |
| 237 | + if (IsClassName(ComPtr<IUIAutomationElement>(node), class_name)) { |
| 238 | + ++count; |
| 239 | + } |
| 240 | + |
| 241 | + ComPtr<IUIAutomationElement> child; |
| 242 | + if (FAILED( |
| 243 | + walker->GetFirstChildElement(node, child.ReleaseAndGetAddressOf()))) |
| 244 | + return; |
| 245 | + |
| 246 | + while (child && visited < visit_limit) { |
| 247 | + dfs(child.Get()); |
| 248 | + ComPtr<IUIAutomationElement> next; |
| 249 | + if (FAILED(walker->GetNextSiblingElement(child.Get(), |
| 250 | + next.ReleaseAndGetAddressOf()))) |
| 251 | + break; |
| 252 | + child = std::move(next); |
| 253 | + } |
| 254 | + }; |
| 255 | + |
| 256 | + dfs(root.Get()); |
| 257 | + if (visited_out) |
| 258 | + *visited_out = visited; |
| 259 | + return count; |
| 260 | +} |
| 261 | + |
| 262 | +ComPtr<IUIAutomationElement> FindRawContainerPreferNonEmpty( |
| 263 | + const ComPtr<IUIAutomation>& automation, |
| 264 | + const ComPtr<IUIAutomationElement>& root, |
| 265 | + std::wstring_view container_class, |
| 266 | + std::wstring_view tab_class, |
| 267 | + std::optional<int>* selected_raw_count_out = nullptr, |
| 268 | + int* selected_raw_visited_out = nullptr) { |
| 269 | + if (!automation || !root) |
| 270 | + return nullptr; |
| 271 | + |
| 272 | + if (selected_raw_count_out) |
| 273 | + *selected_raw_count_out = std::nullopt; |
| 274 | + if (selected_raw_visited_out) |
| 275 | + *selected_raw_visited_out = 0; |
| 276 | + |
| 277 | + ComPtr<IUIAutomationTreeWalker> walker; |
| 278 | + if (FAILED(automation->get_RawViewWalker(&walker)) || !walker) |
| 279 | + return nullptr; |
| 280 | + |
| 281 | + ComPtr<IUIAutomationElement> first_candidate; |
| 282 | + std::optional<int> first_candidate_raw_count; |
| 283 | + int first_candidate_raw_visited = 0; |
| 284 | + int candidate_index = 0; |
| 285 | + |
| 286 | + std::function<ComPtr<IUIAutomationElement>(IUIAutomationElement*)> dfs; |
| 287 | + dfs = [&](IUIAutomationElement* node) -> ComPtr<IUIAutomationElement> { |
| 288 | + if (!node) |
| 289 | + return nullptr; |
| 290 | + |
| 291 | + ComPtr<IUIAutomationElement> current(node); |
| 292 | + if (IsClassName(current, container_class)) { |
| 293 | + ++candidate_index; |
| 294 | + int visited = 0; |
| 295 | + const auto raw_count = CountClassInSubtreeRaw(automation, current, |
| 296 | + tab_class, 50000, &visited); |
| 297 | + |
| 298 | + if (!first_candidate) { |
| 299 | + first_candidate = current; |
| 300 | + first_candidate_raw_count = raw_count; |
| 301 | + first_candidate_raw_visited = visited; |
| 302 | + } |
| 303 | + |
| 304 | + if (raw_count.has_value()) { |
| 305 | + DebugLog( |
| 306 | + L"FindTabCount: RawView candidate #{} container='{}' " |
| 307 | + L"tab_class='{}' " |
| 308 | + L"raw_count={} visited={}", |
| 309 | + candidate_index, container_class, tab_class, raw_count.value(), |
| 310 | + visited); |
| 311 | + if (raw_count.value() > 0) { |
| 312 | + if (selected_raw_count_out) |
| 313 | + *selected_raw_count_out = raw_count; |
| 314 | + if (selected_raw_visited_out) |
| 315 | + *selected_raw_visited_out = visited; |
| 316 | + return current; |
| 317 | + } |
| 318 | + } else { |
| 319 | + DebugLog( |
| 320 | + L"FindTabCount: RawView candidate #{} container='{}' " |
| 321 | + L"tab_class='{}' " |
| 322 | + L"raw_count=<failed>", |
| 323 | + candidate_index, container_class, tab_class); |
| 324 | + } |
| 325 | + } |
| 326 | + |
| 327 | + ComPtr<IUIAutomationElement> child; |
| 328 | + if (FAILED( |
| 329 | + walker->GetFirstChildElement(node, child.ReleaseAndGetAddressOf()))) |
| 330 | + return nullptr; |
| 331 | + |
| 332 | + while (child) { |
| 333 | + if (auto hit = dfs(child.Get())) |
| 334 | + return hit; |
| 335 | + ComPtr<IUIAutomationElement> next; |
| 336 | + if (FAILED(walker->GetNextSiblingElement(child.Get(), |
| 337 | + next.ReleaseAndGetAddressOf()))) |
| 338 | + break; |
| 339 | + child = std::move(next); |
| 340 | + } |
| 341 | + return nullptr; |
| 342 | + }; |
| 343 | + |
| 344 | + if (auto hit = dfs(root.Get())) { |
| 345 | + return hit; |
| 346 | + } |
| 347 | + |
| 348 | + if (first_candidate) { |
| 349 | + DebugLog( |
| 350 | + L"FindTabCount: RawView fallback uses first '{}' candidate (all " |
| 351 | + L"candidates had raw_count=0)", |
| 352 | + container_class); |
| 353 | + |
| 354 | + if (selected_raw_count_out) |
| 355 | + *selected_raw_count_out = first_candidate_raw_count; |
| 356 | + if (selected_raw_visited_out) |
| 357 | + *selected_raw_visited_out = first_candidate_raw_visited; |
| 358 | + } |
| 359 | + return first_candidate; |
| 360 | +} |
| 361 | + |
| 362 | +// 从 HWND 找 tab container,带 fullscreen fallback |
| 363 | +std::optional<TabContainer> FindTabContainerFromHwnd( |
| 364 | + const ComPtr<IUIAutomation>& automation, |
| 365 | + HWND hwnd) { |
| 366 | + DebugLog(L"FindTabCount: FindTabContainerFromHwnd start hwnd={:p}", |
| 367 | + reinterpret_cast<void*>(hwnd)); |
| 368 | + |
| 369 | + auto root = GetElementFromHandle(automation, hwnd); |
| 370 | + if (!root) { |
| 371 | + DebugLog(L"FindTabCount: ElementFromHandle failed hwnd={:p}", |
| 372 | + reinterpret_cast<void*>(hwnd)); |
| 373 | + return std::nullopt; |
| 374 | + } |
| 375 | + |
| 376 | + if (const auto root_class = |
| 377 | + GetCurrentStringProperty(root, UIA_ClassNamePropertyId); |
| 378 | + root_class.has_value()) { |
| 379 | + DebugLog(L"FindTabCount: root class='{}'", root_class.value()); |
| 380 | + } else { |
| 381 | + DebugLog(L"FindTabCount: root class=<unknown>"); |
| 382 | + } |
| 383 | + |
| 384 | + // horizontal |
| 385 | + if (auto c = FindFirstInSubtree( |
| 386 | + root, CreateClassCondition(automation, L"TabContainerImpl"))) { |
| 387 | + DebugLog(L"FindTabCount: container hit ControlView class=TabContainerImpl"); |
| 388 | + return TabContainer{c, L"Tab", false}; |
| 389 | + } |
| 390 | + |
| 391 | + // vertical |
| 392 | + if (auto c = FindFirstInSubtree( |
| 393 | + root, CreateClassCondition(automation, |
| 394 | + L"VerticalUnpinnedTabContainerView"))) { |
| 395 | + DebugLog( |
| 396 | + L"FindTabCount: container hit ControlView " |
| 397 | + L"class=VerticalUnpinnedTabContainerView"); |
| 398 | + return TabContainer{c, L"VerticalTabView", false}; |
| 399 | + } |
| 400 | + |
| 401 | + // Fullscreen |
| 402 | + DebugLog(L"FindTabCount: ControlView miss, trying RawView fallback"); |
| 403 | + std::optional<int> raw_tab_count; |
| 404 | + int raw_tab_visited = 0; |
| 405 | + if (auto c = FindRawContainerPreferNonEmpty( |
| 406 | + automation, root, L"TabContainerImpl", L"Tab", &raw_tab_count, |
| 407 | + &raw_tab_visited)) { |
| 408 | + DebugLog(L"FindTabCount: container hit RawView class=TabContainerImpl"); |
| 409 | + return TabContainer{c, L"Tab", true, raw_tab_count, raw_tab_visited}; |
| 410 | + } |
| 411 | + if (auto c = FindRawContainerPreferNonEmpty( |
| 412 | + automation, root, L"VerticalUnpinnedTabContainerView", |
| 413 | + L"VerticalTabView", &raw_tab_count, &raw_tab_visited)) { |
| 414 | + DebugLog( |
| 415 | + L"FindTabCount: container hit RawView " |
| 416 | + L"class=VerticalUnpinnedTabContainerView"); |
| 417 | + return TabContainer{c, L"VerticalTabView", true, raw_tab_count, |
| 418 | + raw_tab_visited}; |
| 419 | + } |
| 420 | + |
| 421 | + DebugLog(L"FindTabCount: container not found in ControlView or RawView"); |
| 422 | + |
| 423 | + return std::nullopt; |
| 424 | +} |
| 425 | + |
211 | 426 | ComPtr<IUIAutomationElement> FindSiblingByClass( |
212 | 427 | const ComPtr<IUIAutomation>& automation, |
213 | 428 | const ComPtr<IUIAutomationElement>& element, |
@@ -400,6 +615,122 @@ std::optional<TabHitResult> FindTabHitResult(POINT pt, |
400 | 615 | return std::nullopt; |
401 | 616 | } |
402 | 617 |
|
| 618 | +std::optional<int> FindTabCount(HWND hwnd) { |
| 619 | + DebugLog(L"FindTabCount: start hwnd={:p}", reinterpret_cast<void*>(hwnd)); |
| 620 | + |
| 621 | + if (!EnsureAutomation()) { |
| 622 | + DebugLog(L"FindTabCount: EnsureAutomation failed"); |
| 623 | + return std::nullopt; |
| 624 | + } |
| 625 | + const auto& automation = GetUiaThreadState().automation; |
| 626 | + |
| 627 | + const auto tab_container = FindTabContainerFromHwnd(automation, hwnd); |
| 628 | + if (!tab_container) { |
| 629 | + DebugLog(L"FindTabCount: no tab container"); |
| 630 | + return std::nullopt; |
| 631 | + } |
| 632 | + |
| 633 | + DebugLog(L"FindTabCount: tab class='{}' found_via_raw={}", |
| 634 | + tab_container->tab_class, |
| 635 | + tab_container->found_via_raw ? L"true" : L"false"); |
| 636 | + |
| 637 | + const auto condition = |
| 638 | + CreateClassCondition(automation, tab_container->tab_class); |
| 639 | + if (!condition) { |
| 640 | + DebugLog(L"FindTabCount: CreateClassCondition failed class='{}'", |
| 641 | + tab_container->tab_class); |
| 642 | + return std::nullopt; |
| 643 | + } |
| 644 | + |
| 645 | + ComPtr<IUIAutomationElementArray> arr; |
| 646 | + const HRESULT children_hr = tab_container->container->FindAll( |
| 647 | + TreeScope_Children, condition.Get(), arr.ReleaseAndGetAddressOf()); |
| 648 | + if (FAILED(children_hr) || !arr) { |
| 649 | + DebugLog(L"FindTabCount: FindAll(TreeScope_Children) failed hr=0x{:08X}", |
| 650 | + static_cast<unsigned long>(children_hr)); |
| 651 | + |
| 652 | + ComPtr<IUIAutomationElementArray> subtree_arr; |
| 653 | + const HRESULT subtree_hr = |
| 654 | + tab_container->container->FindAll(TreeScope_Subtree, condition.Get(), |
| 655 | + subtree_arr.ReleaseAndGetAddressOf()); |
| 656 | + if (FAILED(subtree_hr) || !subtree_arr) { |
| 657 | + DebugLog( |
| 658 | + L"FindTabCount: FindAll(TreeScope_Subtree) also failed hr=0x{:08X}", |
| 659 | + static_cast<unsigned long>(subtree_hr)); |
| 660 | + } else { |
| 661 | + int subtree_count = 0; |
| 662 | + if (FAILED(subtree_arr->get_Length(&subtree_count))) { |
| 663 | + DebugLog(L"FindTabCount: TreeScope_Subtree get_Length failed"); |
| 664 | + } else { |
| 665 | + DebugLog(L"FindTabCount: TreeScope_Subtree count={}", subtree_count); |
| 666 | + } |
| 667 | + } |
| 668 | + return std::nullopt; |
| 669 | + } |
| 670 | + |
| 671 | + int count = 0; |
| 672 | + if (FAILED(arr->get_Length(&count))) { |
| 673 | + DebugLog(L"FindTabCount: TreeScope_Children get_Length failed"); |
| 674 | + return std::nullopt; |
| 675 | + } |
| 676 | + |
| 677 | + DebugLog(L"FindTabCount: TreeScope_Children count={}", count); |
| 678 | + |
| 679 | + ComPtr<IUIAutomationElementArray> subtree_arr; |
| 680 | + const HRESULT subtree_hr = tab_container->container->FindAll( |
| 681 | + TreeScope_Subtree, condition.Get(), subtree_arr.ReleaseAndGetAddressOf()); |
| 682 | + if (SUCCEEDED(subtree_hr) && subtree_arr) { |
| 683 | + int subtree_count = 0; |
| 684 | + if (SUCCEEDED(subtree_arr->get_Length(&subtree_count))) { |
| 685 | + DebugLog(L"FindTabCount: TreeScope_Subtree count={}", subtree_count); |
| 686 | + } |
| 687 | + } |
| 688 | + |
| 689 | + if (count == 0) { |
| 690 | + std::optional<int> raw_count = tab_container->raw_tab_count; |
| 691 | + int raw_visited = tab_container->raw_tab_visited; |
| 692 | + if (!raw_count.has_value()) { |
| 693 | + raw_count = |
| 694 | + CountClassInSubtreeRaw(automation, tab_container->container, |
| 695 | + tab_container->tab_class, 50000, &raw_visited); |
| 696 | + } |
| 697 | + |
| 698 | + if (raw_count.has_value()) { |
| 699 | + DebugLog(L"FindTabCount: RawWalker count(class='{}')={} visited={}", |
| 700 | + tab_container->tab_class, raw_count.value(), raw_visited); |
| 701 | + if (raw_count.value() > 0) { |
| 702 | + DebugLog(L"FindTabCount: use RawWalker fallback count={}", |
| 703 | + raw_count.value()); |
| 704 | + return raw_count.value(); |
| 705 | + } |
| 706 | + } else { |
| 707 | + DebugLog(L"FindTabCount: RawWalker count(class='{}') failed", |
| 708 | + tab_container->tab_class); |
| 709 | + } |
| 710 | + |
| 711 | + for (const auto probe_class : |
| 712 | + {std::wstring_view(L"Tab"), std::wstring_view(L"VerticalTabView"), |
| 713 | + std::wstring_view(L"TabSlotView"), |
| 714 | + std::wstring_view(L"TabGroupHeader")}) { |
| 715 | + if (probe_class == tab_container->tab_class) { |
| 716 | + continue; |
| 717 | + } |
| 718 | + |
| 719 | + int probe_visited = 0; |
| 720 | + const auto probe_count = |
| 721 | + CountClassInSubtreeRaw(automation, tab_container->container, |
| 722 | + probe_class, 30000, &probe_visited); |
| 723 | + if (probe_count.has_value()) { |
| 724 | + DebugLog( |
| 725 | + L"FindTabCount: RawWalker probe class='{}' count={} visited={}", |
| 726 | + probe_class, probe_count.value(), probe_visited); |
| 727 | + } |
| 728 | + } |
| 729 | + } |
| 730 | + |
| 731 | + return count; |
| 732 | +} |
| 733 | + |
403 | 734 | bool IsOnTabBar(POINT pt) { |
404 | 735 | if (!EnsureAutomation()) { |
405 | 736 | return false; |
|
0 commit comments