@@ -506,99 +506,94 @@ int hasClickAction(void* element) {
506506 if (!element) return 0 ;
507507 AXUIElementRef axElement = (AXUIElementRef)element;
508508
509- // Check explicit actions
509+ // Ignore hidden or disabled elements early
510+ CFBooleanRef hidden = NULL ;
511+ if (AXUIElementCopyAttributeValue (axElement, kAXHiddenAttribute , (CFTypeRef *)&hidden) == kAXErrorSuccess && hidden) {
512+ if (CFBooleanGetValue (hidden)) {
513+ CFRelease (hidden);
514+ return 0 ;
515+ }
516+ CFRelease (hidden);
517+ }
518+
519+ CFBooleanRef enabled = NULL ;
520+ bool isEnabled = true ; // default to true if attribute not present
521+ if (AXUIElementCopyAttributeValue (axElement, kAXEnabledAttribute , (CFTypeRef *)&enabled) == kAXErrorSuccess && enabled) {
522+ isEnabled = CFBooleanGetValue (enabled);
523+ CFRelease (enabled);
524+ }
525+ if (!isEnabled) return 0 ;
526+
527+ // Get role for role-specific fallbacks
528+ CFStringRef role = NULL ;
529+ if (AXUIElementCopyAttributeValue (axElement, kAXRoleAttribute , (CFTypeRef *)&role) != kAXErrorSuccess ) {
530+ role = NULL ;
531+ }
532+
533+ // Explicit actions are the strongest signal
510534 CFArrayRef actions = NULL ;
511535 if (AXUIElementCopyActionNames (axElement, &actions) == kAXErrorSuccess && actions) {
512536 CFIndex count = CFArrayGetCount (actions);
513537 for (CFIndex i = 0 ; i < count; i++) {
514538 CFStringRef action = (CFStringRef)CFArrayGetValueAtIndex (actions, i);
515539 if (CFStringCompare (action, kAXPressAction , 0 ) == kCFCompareEqualTo ||
540+ CFStringCompare (action, CFSTR (" AXShowMenu" ), 0 ) == kCFCompareEqualTo ||
516541 CFStringCompare (action, CFSTR (" AXConfirm" ), 0 ) == kCFCompareEqualTo ||
517542 CFStringCompare (action, CFSTR (" AXPick" ), 0 ) == kCFCompareEqualTo ||
518- CFStringCompare (action, CFSTR (" AXShowMenu " ), 0 ) == kCFCompareEqualTo ) {
543+ CFStringCompare (action, CFSTR (" AXRaise " ), 0 ) == kCFCompareEqualTo ) {
519544 CFRelease (actions);
545+ if (role) CFRelease (role);
520546 return 1 ;
521547 }
522548 }
523549 CFRelease (actions);
524550 }
525551
526- // Focusable + enabled
527- CFBooleanRef enabled = NULL ;
528- if (AXUIElementCopyAttributeValue (axElement, kAXEnabledAttribute , (CFTypeRef *)&enabled) == kAXErrorSuccess && enabled) {
529- if (CFBooleanGetValue (enabled)) {
530- CFRelease (enabled);
531- CFBooleanRef focusable = NULL ;
532- if (AXUIElementCopyAttributeValue (axElement, kAXFocusableAttribute , (CFTypeRef *)&focusable) == kAXErrorSuccess && focusable) {
533- int isFocus = CFBooleanGetValue (focusable);
534- CFRelease (focusable);
535- if (isFocus) return 1 ;
536- }
537- } else CFRelease (enabled);
538- }
539-
540- // Has title or label
541- CFTypeRef title = NULL ;
542- if (AXUIElementCopyAttributeValue (axElement, kAXTitleAttribute , &title) == kAXErrorSuccess && title) {
543- CFRelease (title);
544- return 1 ;
545- }
546-
547- // Has description or help
548- CFTypeRef desc = NULL ;
549- if (AXUIElementCopyAttributeValue (axElement, kAXDescriptionAttribute , &desc) == kAXErrorSuccess && desc) {
550- CFRelease (desc);
552+ // Some elements support AXPress even if not listed in action names
553+ CFStringRef pressDesc = NULL ;
554+ if (AXUIElementCopyActionDescription (axElement, kAXPressAction , &pressDesc) == kAXErrorSuccess && pressDesc) {
555+ CFRelease (pressDesc);
556+ if (role) CFRelease (role);
551557 return 1 ;
552558 }
553-
554- // Has value or selected state
555- CFTypeRef value = NULL ;
556- if (AXUIElementCopyAttributeValue (axElement, kAXValueAttribute , &value) == kAXErrorSuccess && value) {
557- CFRelease (value);
558- return 1 ;
559- }
560-
561- CFTypeRef selected = NULL ;
562- if (AXUIElementCopyAttributeValue (axElement, kAXSelectedAttribute , &selected) == kAXErrorSuccess && selected) {
563- CFRelease (selected);
564- return 1 ;
559+ if (pressDesc) CFRelease (pressDesc);
560+
561+ // Focusable and enabled controls are clickable (e.g., text fields)
562+ CFBooleanRef focusable = NULL ;
563+ if (AXUIElementCopyAttributeValue (axElement, kAXFocusableAttribute , (CFTypeRef *)&focusable) == kAXErrorSuccess && focusable) {
564+ if (CFBooleanGetValue (focusable)) {
565+ CFRelease (focusable);
566+ // Ensure the element has a hittable rect and is not occluded by other apps
567+ CGPoint center;
568+ pid_t pid;
569+ bool visible = true ;
570+ if (getElementCenter ((void *)axElement, ¢er) && AXUIElementGetPid (axElement, &pid) == kAXErrorSuccess ) {
571+ visible = isPointVisible (center, pid);
572+ }
573+ if (role) CFRelease (role);
574+ return visible ? 1 : 0 ;
575+ }
576+ CFRelease (focusable);
565577 }
566578
567- // Has visual bounds
568- CFTypeRef position = NULL ;
569- CFTypeRef size = NULL ;
570- if (AXUIElementCopyAttributeValue (axElement, kAXPositionAttribute , &position ) == kAXErrorSuccess &&
571- AXUIElementCopyAttributeValue (axElement, kAXSizeAttribute , &size) == kAXErrorSuccess &&
572- position && size) {
573- CFRelease (position) ;
574- CFRelease (size);
575- return 1 ;
579+ // Role-specific fallback for links: Treat as clickable if they expose a URL
580+ if (role && CFStringCompare (role, kAXLinkRole , 0 ) == kCFCompareEqualTo ) {
581+ CFTypeRef urlAttr = NULL ;
582+ if (AXUIElementCopyAttributeValue (axElement, kAXURLAttribute , &urlAttr ) == kAXErrorSuccess && urlAttr) {
583+ CFRelease (urlAttr);
584+ CFRelease (role);
585+ return 1 ;
586+ }
587+ if (urlAttr) CFRelease (urlAttr) ;
576588 }
577- if (position) CFRelease (position);
578- if (size) CFRelease (size);
579589
580- // Expanded/collapsible state
581- CFTypeRef expanded = NULL ;
582- if (AXUIElementCopyAttributeValue (axElement, kAXExpandedAttribute , &expanded) == kAXErrorSuccess && expanded) {
583- CFRelease (expanded);
584- return 1 ;
585- }
590+ if (role) CFRelease (role);
586591
587- // Visible + leaf node
588- CFBooleanRef visible = NULL ;
589- if (AXUIElementCopyAttributeValue (axElement, kAXVisibleAttribute , (CFTypeRef *)&visible) == kAXErrorSuccess && visible) {
590- if (CFBooleanGetValue (visible)) {
591- CFArrayRef children = NULL ;
592- if (AXUIElementCopyAttributeValue (axElement, kAXChildrenAttribute , (CFTypeRef *)&children) == kAXErrorSuccess ) {
593- if (children && CFArrayGetCount (children) == 0 ) {
594- CFRelease (children);
595- CFRelease (visible);
596- return 1 ;
597- }
598- if (children) CFRelease (children);
599- }
600- }
601- CFRelease (visible);
592+ // final check, ensure a visible bounding box and not occluded
593+ CGPoint center;
594+ pid_t pid;
595+ if (getElementCenter ((void *)axElement, ¢er) && AXUIElementGetPid (axElement, &pid) == kAXErrorSuccess ) {
596+ return isPointVisible (center, pid) ? 1 : 0 ;
602597 }
603598
604599 return 0 ;
0 commit comments