# Orca
#
# Copyright 2023 Igalia, S.L.
# Author: Joanmarie Diggs <jdiggs@igalia.com>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA  02110-1301 USA.

# pylint: disable=too-many-public-methods
# pylint: disable=too-many-lines

"""Utilities for accessible roles."""

from __future__ import annotations

import gi

gi.require_version("Atspi", "2.0")
from gi.repository import Atspi

from . import debug, object_properties
from .ax_collection import AXCollection
from .ax_object import AXObject
from .ax_utilities_action import AXUtilitiesAction
from .ax_utilities_object import AXUtilitiesObject
from .ax_utilities_state import AXUtilitiesState


class AXUtilitiesRole:
    """Utilities for accessible roles."""

    _TEXT_BLOCK_ROLES: frozenset[Atspi.Role] | None = None

    _MATH_TAGS = frozenset(
        (
            "math",
            "maction",
            "maligngroup",
            "malignmark",
            "menclose",
            "merror",
            "mfenced",
            "mfrac",
            "mglyph",
            "mi",
            "mlabeledtr",
            "mlongdiv",
            "mmultiscripts",
            "mn",
            "mo",
            "mover",
            "mpadded",
            "mphantom",
            "mprescripts",
            "mroot",
            "mrow",
            "ms",
            "mscarries",
            "mscarry",
            "msgroup",
            "msline",
            "mspace",
            "msqrt",
            "msrow",
            "mstack",
            "mstyle",
            "msub",
            "msup",
            "msubsup",
            "mtable",
            "mtd",
            "mtext",
            "mtr",
            "munder",
            "munderover",
        )
    )

    @staticmethod
    def _get_display_style(obj: Atspi.Accessible) -> str:
        attrs = AXObject.get_attributes_dict(obj)
        return attrs.get("display", "")

    @staticmethod
    def _get_tag(obj: Atspi.Accessible) -> str | None:
        attrs = AXObject.get_attributes_dict(obj)
        return attrs.get("tag")

    @staticmethod
    def _get_xml_roles(obj: Atspi.Accessible) -> list[str]:
        attrs = AXObject.get_attributes_dict(obj)
        return attrs.get("xml-roles", "").split()

    @staticmethod
    def children_are_presentational(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if the descendants of obj should be ignored. See ARIA spec."""

        # Note: We are deliberately leaving out listbox options because they can be complex,
        # both in ARIA and in GTK.

        roles = [
            Atspi.Role.BUTTON,
            Atspi.Role.CHECK_BOX,
            Atspi.Role.CHECK_MENU_ITEM,
            Atspi.Role.IMAGE,
            Atspi.Role.LEVEL_BAR,
            Atspi.Role.PAGE_TAB,
            Atspi.Role.PROGRESS_BAR,
            Atspi.Role.RADIO_BUTTON,
            Atspi.Role.RADIO_MENU_ITEM,
            Atspi.Role.SCROLL_BAR,
            Atspi.Role.SEPARATOR,
            Atspi.Role.SLIDER,
            Atspi.Role.SWITCH,
            Atspi.Role.TOGGLE_BUTTON,
        ]

        if role is None:
            role = AXObject.get_role(obj)

        if role in roles:
            tokens = ["AXUtilitiesRole:", obj, "has presentational children."]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            return True

        return False

    @staticmethod
    def get_annotation_roles() -> list[Atspi.Role]:
        """Returns the list of roles we consider annotations."""

        # TODO - JD: Enable suggestion once we bump to a version of AT-SPI2 that contains the
        # fix for https://gitlab.gnome.org/GNOME/at-spi2-core/-/issues/222.
        roles = [
            Atspi.Role.CONTENT_DELETION,
            Atspi.Role.CONTENT_INSERTION,
            Atspi.Role.MARK,
            # Atspi.Role.SUGGESTION,
        ]
        return roles

    @staticmethod
    def get_dialog_roles(include_alert_as_dialog: bool = True) -> list[Atspi.Role]:
        """Returns the list of roles we consider documents"""

        roles = [Atspi.Role.COLOR_CHOOSER, Atspi.Role.DIALOG, Atspi.Role.FILE_CHOOSER]
        if include_alert_as_dialog:
            roles.append(Atspi.Role.ALERT)
        return roles

    @staticmethod
    def get_document_roles() -> list[Atspi.Role]:
        """Returns the list of roles we consider documents"""

        roles = [
            Atspi.Role.DOCUMENT_EMAIL,
            Atspi.Role.DOCUMENT_FRAME,
            Atspi.Role.DOCUMENT_PRESENTATION,
            Atspi.Role.DOCUMENT_SPREADSHEET,
            Atspi.Role.DOCUMENT_TEXT,
            Atspi.Role.DOCUMENT_WEB,
        ]
        return roles

    @staticmethod
    def get_form_field_roles() -> list[Atspi.Role]:
        """Returns the list of roles we consider form fields"""

        roles = [
            Atspi.Role.BUTTON,
            Atspi.Role.CHECK_BOX,
            Atspi.Role.RADIO_BUTTON,
            Atspi.Role.COMBO_BOX,
            Atspi.Role.DOCUMENT_FRAME,  # rich text editing pred recommended
            Atspi.Role.TEXT,  # predicate recommended to check it is editable
            Atspi.Role.LIST_BOX,
            Atspi.Role.ENTRY,
            Atspi.Role.PASSWORD_TEXT,
            Atspi.Role.SPIN_BUTTON,
            Atspi.Role.TOGGLE_BUTTON,
        ]
        return roles

    @staticmethod
    def get_large_container_roles() -> list[Atspi.Role]:
        """Returns the list of roles we consider a large container."""

        # Note: We are deliberately leaving out sections because those are often DIVs
        # which are generic and often not large. The primary consumer of this function
        # is structural navigation which uses it for the jump-to-edge functionality.
        roles = [
            Atspi.Role.ARTICLE,
            Atspi.Role.BLOCK_QUOTE,
            Atspi.Role.DESCRIPTION_LIST,
            Atspi.Role.FORM,
            Atspi.Role.FOOTER,
            Atspi.Role.GROUPING,
            Atspi.Role.HEADER,
            Atspi.Role.HTML_CONTAINER,
            Atspi.Role.LANDMARK,
            Atspi.Role.LOG,
            Atspi.Role.LIST,
            Atspi.Role.MARQUEE,
            Atspi.Role.PANEL,
            Atspi.Role.TABLE,
            Atspi.Role.TREE,
            Atspi.Role.TREE_TABLE,
        ]

        return roles

    @staticmethod
    def get_layout_only_roles() -> list[Atspi.Role]:
        """Returns the list of roles we consider are for layout only"""

        roles = [
            Atspi.Role.AUTOCOMPLETE,
            Atspi.Role.FILLER,
            Atspi.Role.REDUNDANT_OBJECT,
            Atspi.Role.UNKNOWN,
            Atspi.Role.SCROLL_PANE,
            Atspi.Role.TEAROFF_MENU_ITEM,
        ]
        return roles

    @staticmethod
    def get_menu_item_roles() -> list[Atspi.Role]:
        """Returns the list of roles we consider menu items"""

        roles = [
            Atspi.Role.MENU_ITEM,
            Atspi.Role.CHECK_MENU_ITEM,
            Atspi.Role.RADIO_MENU_ITEM,
            Atspi.Role.TEAROFF_MENU_ITEM,
        ]
        return roles

    @staticmethod
    def get_menu_related_roles() -> list[Atspi.Role]:
        """Returns the list of roles we consider menu related"""

        roles = [
            Atspi.Role.MENU,
            Atspi.Role.MENU_BAR,
            Atspi.Role.POPUP_MENU,
            Atspi.Role.MENU_ITEM,
            Atspi.Role.CHECK_MENU_ITEM,
            Atspi.Role.RADIO_MENU_ITEM,
            Atspi.Role.TEAROFF_MENU_ITEM,
        ]
        return roles

    @staticmethod
    def get_roles_to_exclude_from_clickables_list() -> list[Atspi.Role]:
        """Returns the list of roles we want to exclude from the list of clickables"""

        roles = [
            Atspi.Role.COMBO_BOX,
            Atspi.Role.ENTRY,
            Atspi.Role.LIST_BOX,
            Atspi.Role.MENU,
            Atspi.Role.MENU_ITEM,
            Atspi.Role.CHECK_MENU_ITEM,
            Atspi.Role.RADIO_MENU_ITEM,
            Atspi.Role.PAGE_TAB,
            Atspi.Role.PAGE_TAB_LIST,
            Atspi.Role.PASSWORD_TEXT,
            Atspi.Role.PROGRESS_BAR,
            Atspi.Role.SLIDER,
            Atspi.Role.SPIN_BUTTON,
            Atspi.Role.TOOL_BAR,
            Atspi.Role.TREE_ITEM,
            Atspi.Role.TREE_TABLE,
            Atspi.Role.TREE,
        ]
        return roles

    @staticmethod
    def get_set_container_roles() -> list[Atspi.Role]:
        """Returns the list of roles we consider a set container"""

        roles = [
            Atspi.Role.LIST,
            Atspi.Role.MENU,
            Atspi.Role.PAGE_TAB_LIST,
            Atspi.Role.TABLE,
            Atspi.Role.TREE,
            Atspi.Role.TREE_TABLE,
        ]
        return roles

    @staticmethod
    def get_table_cell_roles(include_headers: bool = True) -> list[Atspi.Role]:
        """Returns the list of roles we consider table cells"""

        roles = [Atspi.Role.TABLE_CELL]
        if include_headers:
            roles.extend(
                [
                    Atspi.Role.TABLE_COLUMN_HEADER,
                    Atspi.Role.TABLE_ROW_HEADER,
                    Atspi.Role.COLUMN_HEADER,
                    Atspi.Role.ROW_HEADER,
                ],
            )
        return roles

    @staticmethod
    def get_table_header_roles() -> list[Atspi.Role]:
        """Returns the list of roles we consider table headers"""

        roles = [
            Atspi.Role.TABLE_COLUMN_HEADER,
            Atspi.Role.TABLE_ROW_HEADER,
            Atspi.Role.COLUMN_HEADER,
            Atspi.Role.ROW_HEADER,
        ]
        return roles

    @staticmethod
    def get_table_related_roles(include_caption: bool = False) -> list[Atspi.Role]:
        """Returns the list of roles we consider table related"""

        roles = [
            Atspi.Role.TABLE,
            Atspi.Role.TABLE_CELL,
            Atspi.Role.TABLE_COLUMN_HEADER,
            Atspi.Role.TABLE_ROW_HEADER,
            Atspi.Role.COLUMN_HEADER,
            Atspi.Role.ROW_HEADER,
        ]
        if include_caption:
            roles.append(Atspi.Role.CAPTION)
        return roles

    @staticmethod
    def get_text_block_roles() -> frozenset[Atspi.Role]:
        """Returns the set of roles we consider text blocks."""

        if AXUtilitiesRole._TEXT_BLOCK_ROLES is None:
            AXUtilitiesRole._TEXT_BLOCK_ROLES = frozenset(
                {
                    Atspi.Role.ARTICLE,
                    Atspi.Role.CAPTION,
                    Atspi.Role.COLUMN_HEADER,
                    Atspi.Role.COMMENT,
                    Atspi.Role.CONTENT_DELETION,
                    Atspi.Role.CONTENT_INSERTION,
                    Atspi.Role.DEFINITION,
                    Atspi.Role.DESCRIPTION_LIST,
                    Atspi.Role.DESCRIPTION_TERM,
                    Atspi.Role.DESCRIPTION_VALUE,
                    Atspi.Role.DOCUMENT_FRAME,
                    Atspi.Role.DOCUMENT_WEB,
                    Atspi.Role.FOOTER,
                    Atspi.Role.FORM,
                    Atspi.Role.HEADING,
                    Atspi.Role.LIST,
                    Atspi.Role.LIST_ITEM,
                    Atspi.Role.MARK,
                    Atspi.Role.PARAGRAPH,
                    Atspi.Role.ROW_HEADER,
                    Atspi.Role.SECTION,
                    Atspi.Role.STATIC,
                    Atspi.Role.SUGGESTION,
                    Atspi.Role.TABLE_CELL,
                    Atspi.Role.TEXT,
                }
            )
        return AXUtilitiesRole._TEXT_BLOCK_ROLES

    @staticmethod
    def get_text_ui_roles() -> list[Atspi.Role]:
        """Returns the list of roles we consider UI that displays static text"""

        roles = [Atspi.Role.INFO_BAR, Atspi.Role.LABEL, Atspi.Role.PAGE_TAB, Atspi.Role.STATUS_BAR]
        return roles

    @staticmethod
    def get_tree_related_roles() -> list[Atspi.Role]:
        """Returns the list of roles we consider tree related"""

        roles = [Atspi.Role.TREE, Atspi.Role.TREE_ITEM, Atspi.Role.TREE_TABLE]
        return roles

    @staticmethod
    def get_widget_roles() -> list[Atspi.Role]:
        """Returns the list of roles we consider widgets"""

        roles = [
            Atspi.Role.BUTTON,
            Atspi.Role.CHECK_BOX,
            Atspi.Role.COMBO_BOX,
            Atspi.Role.ENTRY,
            Atspi.Role.LIST_BOX,
            Atspi.Role.PASSWORD_TEXT,
            Atspi.Role.RADIO_BUTTON,
            Atspi.Role.SLIDER,
            Atspi.Role.SPIN_BUTTON,
            Atspi.Role.SWITCH,
            Atspi.Role.TEXT,  # predicate recommended to check it is editable
            Atspi.Role.TOGGLE_BUTTON,
        ]
        return roles

    @staticmethod
    def _get_value_role_name(obj: Atspi.Accessible, role: Atspi.Role) -> str:
        """Returns the localized role name for a value-supporting object, or empty string."""

        name = ""
        if AXUtilitiesRole.is_horizontal_slider(obj, role):
            name = object_properties.ROLE_SLIDER_HORIZONTAL
        elif AXUtilitiesRole.is_vertical_slider(obj, role):
            name = object_properties.ROLE_SLIDER_VERTICAL
        elif AXUtilitiesRole.is_horizontal_scrollbar(obj, role):
            name = object_properties.ROLE_SCROLL_BAR_HORIZONTAL
        elif AXUtilitiesRole.is_vertical_scrollbar(obj, role):
            name = object_properties.ROLE_SCROLL_BAR_VERTICAL
        elif AXUtilitiesRole.is_horizontal_separator(obj, role):
            name = object_properties.ROLE_SPLITTER_HORIZONTAL
        elif AXUtilitiesRole.is_vertical_separator(obj, role):
            name = object_properties.ROLE_SPLITTER_VERTICAL
        elif AXUtilitiesRole.is_split_pane(obj, role):
            # The splitter has the opposite orientation of the split pane.
            if AXObject.has_state(obj, Atspi.StateType.HORIZONTAL):
                name = object_properties.ROLE_SPLITTER_VERTICAL
            elif AXObject.has_state(obj, Atspi.StateType.VERTICAL):
                name = object_properties.ROLE_SPLITTER_HORIZONTAL
        return name

    @staticmethod
    def _get_dpub_role_name(obj: Atspi.Accessible, role: Atspi.Role) -> str:
        """Returns the localized role name for a DPUB object, or empty string."""

        name = ""
        if AXUtilitiesRole.is_landmark(obj, role):
            name = AXUtilitiesRole._get_dpub_landmark_role_name(obj, role)
        elif role == "ROLE_DPUB_SECTION":
            name = AXUtilitiesRole._get_dpub_section_role_name(obj, role)
        elif AXUtilitiesRole.is_list_item(obj, role):
            if AXUtilitiesRole.is_dpub_biblioref(obj, role):
                name = object_properties.ROLE_BIBLIOENTRY
            elif AXUtilitiesRole.is_dpub_endnote(obj, role):
                name = object_properties.ROLE_ENDNOTE
        elif AXUtilitiesRole.is_dpub_cover(obj, role):
            name = object_properties.ROLE_COVER
        elif AXUtilitiesRole.is_dpub_pagebreak(obj, role):
            name = object_properties.ROLE_PAGEBREAK
        elif AXUtilitiesRole.is_dpub_subtitle(obj, role):
            name = object_properties.ROLE_SUBTITLE
        return name

    @staticmethod
    def _get_dpub_landmark_role_name(obj: Atspi.Accessible, role: Atspi.Role) -> str:
        """Returns the localized role name for a DPUB landmark, or empty string."""

        checks = (
            (AXUtilitiesRole.is_dpub_acknowledgments, object_properties.ROLE_ACKNOWLEDGMENTS),
            (AXUtilitiesRole.is_dpub_afterword, object_properties.ROLE_AFTERWORD),
            (AXUtilitiesRole.is_dpub_appendix, object_properties.ROLE_APPENDIX),
            (AXUtilitiesRole.is_dpub_bibliography, object_properties.ROLE_BIBLIOGRAPHY),
            (AXUtilitiesRole.is_dpub_chapter, object_properties.ROLE_CHAPTER),
            (AXUtilitiesRole.is_dpub_conclusion, object_properties.ROLE_CONCLUSION),
            (AXUtilitiesRole.is_dpub_credits, object_properties.ROLE_CREDITS),
            (AXUtilitiesRole.is_dpub_endnotes, object_properties.ROLE_ENDNOTES),
            (AXUtilitiesRole.is_dpub_epilogue, object_properties.ROLE_EPILOGUE),
            (AXUtilitiesRole.is_dpub_errata, object_properties.ROLE_ERRATA),
            (AXUtilitiesRole.is_dpub_foreword, object_properties.ROLE_FOREWORD),
            (AXUtilitiesRole.is_dpub_glossary, object_properties.ROLE_GLOSSARY),
            (AXUtilitiesRole.is_dpub_index, object_properties.ROLE_INDEX),
            (AXUtilitiesRole.is_dpub_introduction, object_properties.ROLE_INTRODUCTION),
            (AXUtilitiesRole.is_dpub_pagelist, object_properties.ROLE_PAGELIST),
            (AXUtilitiesRole.is_dpub_part, object_properties.ROLE_PART),
            (AXUtilitiesRole.is_dpub_preface, object_properties.ROLE_PREFACE),
            (AXUtilitiesRole.is_dpub_prologue, object_properties.ROLE_PROLOGUE),
            (AXUtilitiesRole.is_dpub_toc, object_properties.ROLE_TOC),
        )
        for check, name in checks:
            if check(obj, role):
                return name
        return ""

    @staticmethod
    def _get_dpub_section_role_name(obj: Atspi.Accessible, role: Atspi.Role) -> str:
        """Returns the localized role name for a DPUB section, or empty string."""

        name = ""
        if AXUtilitiesRole.is_dpub_abstract(obj, role):
            name = object_properties.ROLE_ABSTRACT
        elif AXUtilitiesRole.is_dpub_colophon(obj, role):
            name = object_properties.ROLE_COLOPHON
        elif AXUtilitiesRole.is_dpub_credit(obj, role):
            name = object_properties.ROLE_CREDIT
        elif AXUtilitiesRole.is_dpub_dedication(obj, role):
            name = object_properties.ROLE_DEDICATION
        elif AXUtilitiesRole.is_dpub_epigraph(obj, role):
            name = object_properties.ROLE_EPIGRAPH
        elif AXUtilitiesRole.is_dpub_example(obj, role):
            name = object_properties.ROLE_EXAMPLE
        elif AXUtilitiesRole.is_dpub_pullquote(obj, role):
            name = object_properties.ROLE_PULLQUOTE
        elif AXUtilitiesRole.is_dpub_qna(obj, role):
            name = object_properties.ROLE_QNA
        return name

    @staticmethod
    def _get_landmark_role_name(obj: Atspi.Accessible, role: Atspi.Role) -> str:
        """Returns the localized role name for a landmark, or empty string."""

        name = ""
        if AXUtilitiesRole.is_landmark_without_type(obj, role):
            pass
        elif AXUtilitiesRole.is_landmark_banner(obj, role):
            name = object_properties.ROLE_LANDMARK_BANNER
        elif AXUtilitiesRole.is_landmark_complementary(obj, role):
            name = object_properties.ROLE_LANDMARK_COMPLEMENTARY
        elif AXUtilitiesRole.is_landmark_contentinfo(obj, role):
            name = object_properties.ROLE_LANDMARK_CONTENTINFO
        elif AXUtilitiesRole.is_landmark_main(obj, role):
            name = object_properties.ROLE_LANDMARK_MAIN
        elif AXUtilitiesRole.is_landmark_navigation(obj, role):
            name = object_properties.ROLE_LANDMARK_NAVIGATION
        elif AXUtilitiesRole.is_landmark_region(obj, role):
            name = object_properties.ROLE_LANDMARK_REGION
        elif AXUtilitiesRole.is_landmark_search(obj, role):
            name = object_properties.ROLE_LANDMARK_SEARCH
        return name

    @staticmethod
    def _get_misc_role_name(obj: Atspi.Accessible, role: Atspi.Role) -> str:
        """Returns the localized role name for misc role overrides, or empty string."""

        name = ""
        if AXUtilitiesRole.is_code_block(obj):
            name = object_properties.ROLE_CODE_BLOCK
        elif AXUtilitiesRole.is_suggestion(obj, role):
            name = object_properties.ROLE_CONTENT_SUGGESTION
        elif AXUtilitiesRole.is_feed(obj, role):
            name = object_properties.ROLE_FEED
        elif AXUtilitiesRole.is_figure(obj, role):
            name = object_properties.ROLE_FIGURE
        elif AXUtilitiesRole.is_switch(obj, role):
            name = object_properties.ROLE_SWITCH
        return name

    @staticmethod
    def get_localized_role_name(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> str:
        """Returns a string representing the localized role name of obj."""

        if role is None:
            role = AXObject.get_role(obj)

        if AXObject.supports_value(obj):
            name = AXUtilitiesRole._get_value_role_name(obj, role)
            if name:
                return name

        name = AXUtilitiesRole._get_misc_role_name(obj, role)
        if name:
            return name

        if AXUtilitiesRole.is_dpub(obj):
            name = AXUtilitiesRole._get_dpub_role_name(obj, role)
            if name:
                return name

        if AXUtilitiesRole.is_landmark(obj, role):
            name = AXUtilitiesRole._get_landmark_role_name(obj, role)
            if name:
                return name
            if AXUtilitiesRole.is_landmark_form(obj, role):
                role = Atspi.Role.FORM
        elif AXUtilitiesRole.is_comment(obj, role):
            role = Atspi.Role.COMMENT

        if not isinstance(role, Atspi.Role):
            return AXObject.get_role_name(obj, True)

        return Atspi.role_get_localized_name(role)

    @staticmethod
    def has_live_region_role(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has a role that is used for a live region."""

        # Technically, any role can be a live region. But the work required to determine if an
        # object is a presentable live region is not cheap: We need to obtain the object attributes
        # and we cannot use a cached version because the aria-live value can change. And the reason
        # we need to do this check is to prevent the event manager's spam filtering from treating
        # these events as spam, i.e. during an event flood. Therefore, the list below can be
        # updated on a case-by-case basis but should remain small.

        if role is None:
            role = AXObject.get_role(obj)

        # Gecko maps the ARIA alert role to the alert role (a type of dialog) rather than to
        # the notification role specified by the Core-AAM.
        roles = [
            Atspi.Role.ALERT,
            Atspi.Role.LOG,
            Atspi.Role.NOTIFICATION,
            Atspi.Role.PARAGRAPH,
            Atspi.Role.SECTION,
            Atspi.Role.STATUS_BAR,
        ]
        return role in roles

    @staticmethod
    def has_role_from_aria(obj: Atspi.Accessible) -> bool:
        """Returns True if obj's role comes from ARIA"""

        return bool(AXUtilitiesRole._get_xml_roles(obj))

    @staticmethod
    def have_same_role(obj1: Atspi.Accessible, obj2: Atspi.Accessible) -> bool:
        """Returns True if obj1 and obj2 have the same role"""

        return AXObject.get_role(obj1) == AXObject.get_role(obj2)

    @staticmethod
    def is_accelerator_label(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the accelerator label role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.ACCELERATOR_LABEL

    @staticmethod
    def is_alert(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the alert (a type of dialog) role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.ALERT

    @staticmethod
    def is_aria_alert(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj is an ARIA alert (should have notification role)"""

        if "alert" not in AXUtilitiesRole._get_xml_roles(obj):
            return False

        if role is None:
            role = AXObject.get_role(obj)

        if role != Atspi.Role.NOTIFICATION:
            tokens = ["AXUtilitiesRole: Unexpected role for ARIA alert", obj]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)

        return True

    @staticmethod
    def is_animation(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the animation role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.ANIMATION

    @staticmethod
    def is_application(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the application role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.APPLICATION

    @staticmethod
    def is_arrow(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the arrow role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.ARROW

    @staticmethod
    def is_anchor(obj: Atspi.Accessible) -> bool:
        """Returns True if obj is an anchor."""

        return (
            AXUtilitiesRole.is_link(obj)
            and not AXUtilitiesState.is_focusable(obj)
            and not AXUtilitiesAction.has_action(obj, "jump")
            and not AXUtilitiesRole.has_role_from_aria(obj)
        )

    @staticmethod
    def is_article(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the article role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.ARTICLE

    @staticmethod
    def is_audio(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the audio role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.AUDIO

    @staticmethod
    def is_autocomplete(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the autocomplete role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.AUTOCOMPLETE

    @staticmethod
    def is_block_quote(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the block quote role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.BLOCK_QUOTE or AXUtilitiesRole._get_tag(obj) == "blockquote"

    @staticmethod
    def is_button(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the push- or toggle-button role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role in [Atspi.Role.BUTTON, Atspi.Role.TOGGLE_BUTTON]

    @staticmethod
    def is_button_with_popup(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the push- or toggle-button role and a popup"""

        if not AXUtilitiesRole.is_button(obj, role):
            return False
        return AXUtilitiesState.has_popup(obj)

    @staticmethod
    def is_calendar(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the calendar role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.CALENDAR

    @staticmethod
    def is_canvas(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the canvas role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.CANVAS

    @staticmethod
    def is_caption(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the caption role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.CAPTION

    @staticmethod
    def is_chart(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the chart role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.CHART

    @staticmethod
    def is_check_box(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the checkbox role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.CHECK_BOX

    @staticmethod
    def is_check_menu_item(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the check menuitem role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.CHECK_MENU_ITEM

    @staticmethod
    def is_code(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the code or code-like role"""

        if "code" in AXUtilitiesRole._get_xml_roles(obj):
            return True
        tag = AXUtilitiesRole._get_tag(obj)
        if tag == "code":
            return True
        # Thunderbird uses <pre class="moz-quote-pre"> for quoted email replies.
        if tag == "pre":
            return "quote" not in AXObject.get_attributes_dict(obj).get("class", "")
        return False

    @staticmethod
    def is_code_block(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj is a block-level code element."""

        if not AXUtilitiesRole.is_code(obj):
            return False
        if AXUtilitiesRole._get_tag(obj) == "pre":
            if AXObject.supports_collection(obj):
                rule = AXCollection.create_match_rule(roles=[Atspi.Role.PARAGRAPH])
                return AXCollection.get_first_match(obj, rule) is None
            return AXUtilitiesObject.find_descendant(obj, AXUtilitiesRole.is_paragraph) is None
        return "inline" not in AXUtilitiesRole._get_display_style(obj)

    @staticmethod
    def is_color_chooser(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the color_chooser role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.COLOR_CHOOSER

    @staticmethod
    def is_column_header(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the column header role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.COLUMN_HEADER

    @staticmethod
    def is_combo_box(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the combobox role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.COMBO_BOX

    @staticmethod
    def is_combo_box_popup(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj is a popup for a combo box."""

        # Note: This is currently based on GTK with the goal of not doing a full presentation
        # in response to the window-activate events we get from combo box popups. This function
        # could be modified to identify other combo box popups should the need arise.

        if not AXUtilitiesRole.is_window(obj, role):
            return False

        attrs = AXObject.get_attributes_dict(obj)
        return attrs.get("window-type") == "combo"

    @staticmethod
    def is_comment(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the comment role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.COMMENT or "comment" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_content_deletion(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the content deletion role"""

        if role is None:
            role = AXObject.get_role(obj)
        return (
            role == Atspi.Role.CONTENT_DELETION
            or "deletion" in AXUtilitiesRole._get_xml_roles(obj)
            or AXUtilitiesRole._get_tag(obj) == "del"
        )

    @staticmethod
    def is_content_insertion(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the content insertion role"""

        if role is None:
            role = AXObject.get_role(obj)
        return (
            role == Atspi.Role.CONTENT_INSERTION
            or "insertion" in AXUtilitiesRole._get_xml_roles(obj)
            or AXUtilitiesRole._get_tag(obj) == "ins"
        )

    @staticmethod
    def is_date_editor(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the date editor role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.DATE_EDITOR

    @staticmethod
    def is_default_button(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the push button role the is-default state"""

        return AXUtilitiesRole.is_push_button(obj, role) and AXObject.has_state(
            obj,
            Atspi.StateType.IS_DEFAULT,
        )

    @staticmethod
    def is_definition(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the definition role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.DEFINITION

    @staticmethod
    def is_description_list(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the description list role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.DESCRIPTION_LIST or AXUtilitiesRole._get_tag(obj) == "dl"

    @staticmethod
    def is_description_term(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the description term role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.DESCRIPTION_TERM or AXUtilitiesRole._get_tag(obj) == "dt"

    @staticmethod
    def is_description_value(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the description value role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.DESCRIPTION_VALUE or AXUtilitiesRole._get_tag(obj) == "dd"

    @staticmethod
    def is_desktop_frame(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the desktop frame role"""

        if role is None:
            role = AXObject.get_role(obj)
        if role == Atspi.Role.DESKTOP_FRAME:
            return True
        if role == Atspi.Role.FRAME:
            attrs = AXObject.get_attributes_dict(obj)
            return attrs.get("is-desktop") == "true"
        return False

    @staticmethod
    def is_desktop_icon(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the desktop icon role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.DESKTOP_ICON

    @staticmethod
    def is_dial(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the dial role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.DIAL

    @staticmethod
    def is_dialog(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the dialog role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.DIALOG

    @staticmethod
    def is_dialog_or_alert(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has any dialog or alert role"""

        roles = AXUtilitiesRole.get_dialog_roles(True)
        if role is None:
            role = AXObject.get_role(obj)
        return role in roles

    @staticmethod
    def is_dialog_or_window(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has any dialog or window-related role"""

        roles = AXUtilitiesRole.get_dialog_roles(False)
        roles.extend((Atspi.Role.FRAME, Atspi.Role.WINDOW))
        if role is None:
            role = AXObject.get_role(obj)
        return role in roles

    @staticmethod
    def is_directory_pane(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the directory pane role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.DIRECTORY_PANE

    @staticmethod
    def is_docked_frame(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the frame role and is docked."""

        if not AXUtilitiesRole.is_frame(obj, role):
            return False

        attrs = AXObject.get_attributes_dict(obj)
        return attrs.get("window-type") == "dock"

    @staticmethod
    def is_document(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has any document-related role"""

        roles = AXUtilitiesRole.get_document_roles()
        if role is None:
            role = AXObject.get_role(obj)
        return role in roles

    @staticmethod
    def is_editable_document(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has a document role and the editable state"""

        return AXUtilitiesRole.is_document(obj, role) and AXObject.has_state(
            obj,
            Atspi.StateType.EDITABLE,
        )

    @staticmethod
    def is_document_email(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the document email role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.DOCUMENT_EMAIL

    @staticmethod
    def is_document_frame(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the document frame role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.DOCUMENT_FRAME

    @staticmethod
    def is_document_presentation(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the document presentation role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.DOCUMENT_PRESENTATION

    @staticmethod
    def is_document_spreadsheet(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the document spreadsheet role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.DOCUMENT_SPREADSHEET

    @staticmethod
    def is_document_text(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the document text role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.DOCUMENT_TEXT

    @staticmethod
    def is_document_web(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the document web role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.DOCUMENT_WEB

    @staticmethod
    def is_document_list(obj: Atspi.Accessible) -> bool:
        """Returns True if obj is a list inside a document."""

        if not (AXUtilitiesRole.is_list(obj) or AXUtilitiesRole.is_description_list(obj)):
            return False
        return AXUtilitiesObject.find_ancestor(obj, AXUtilitiesRole.is_document) is not None

    @staticmethod
    def is_document_panel(obj: Atspi.Accessible) -> bool:
        """Returns True if obj is a panel inside a document."""

        if not AXUtilitiesRole.is_panel(obj):
            return False
        return AXUtilitiesObject.find_ancestor(obj, AXUtilitiesRole.is_document) is not None

    @staticmethod
    def is_dpub(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has a DPub role."""

        roles = AXUtilitiesRole._get_xml_roles(obj)
        rv = bool(list(filter(lambda x: x.startswith("doc-"), roles)))
        return rv

    @staticmethod
    def is_dpub_abstract(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub abstract role."""

        return "doc-abstract" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_acknowledgments(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub acknowledgments role."""

        return "doc-acknowledgments" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_afterword(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub afterword role."""

        return "doc-afterword" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_appendix(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub appendix role."""

        return "doc-appendix" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_backlink(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub backlink role."""

        return "doc-backlink" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_biblioref(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub biblioref role."""

        return "doc-biblioref" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_bibliography(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub bibliography role."""

        return "doc-bibliography" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_chapter(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub chapter role."""

        return "doc-chapter" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_colophon(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub colophon role."""

        return "doc-colophon" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_conclusion(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub conclusion role."""

        return "doc-conclusion" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_cover(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub cover role."""

        return "doc-cover" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_credit(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub credit role."""

        return "doc-credit" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_credits(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub credits role."""

        return "doc-credits" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_dedication(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub dedication role."""

        return "doc-dedication" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_endnote(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub endnote role."""

        return "doc-endnote" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_endnotes(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub endnotes role."""

        return "doc-endnotes" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_epigraph(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub epigraph role."""

        return "doc-epigraph" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_epilogue(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub epilogue role."""

        return "doc-epilogue" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_errata(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub errata role."""

        return "doc-errata" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_example(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub example role."""

        return "doc-example" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_footnote(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub footnote role."""

        return "doc-footnote" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_foreword(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub foreword role."""

        return "doc-foreword" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_glossary(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub glossary role."""

        return "doc-glossary" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_glossref(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub glossref role."""

        return "doc-glossref" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_index(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub index role."""

        return "doc-index" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_introduction(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub introduction role."""

        return "doc-introduction" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_noteref(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub noteref role."""

        return "doc-noteref" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_pagelist(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub pagelist role."""

        return "doc-pagelist" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_pagebreak(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub pagebreak role."""

        return "doc-pagebreak" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_part(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub part role."""

        return "doc-part" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_preface(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub preface role."""

        return "doc-preface" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_prologue(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub prologue role."""

        return "doc-prologue" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_pullquote(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub pullquote role."""

        return "doc-pullquote" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_qna(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub qna role."""

        return "doc-qna" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_subtitle(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub subtitle role."""

        return "doc-subtitle" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_dpub_toc(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the DPub toc role."""

        return "doc-toc" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_drawing_area(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the drawing area role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.DRAWING_AREA

    @staticmethod
    def is_editable_combo_box(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj is an editable combobox"""

        if role is None:
            role = AXObject.get_role(obj)
        if role != Atspi.Role.COMBO_BOX:
            return False
        if AXUtilitiesState.is_editable(obj):
            return True
        if AXObject.supports_collection(obj):
            rule = AXCollection.create_match_rule(
                roles=[Atspi.Role.ENTRY, Atspi.Role.PASSWORD_TEXT, Atspi.Role.SPIN_BUTTON],
                role_match_type=Atspi.CollectionMatchType.ANY,
            )
            if AXCollection.get_first_match(obj, rule) is not None:
                return True
            rule = AXCollection.create_match_rule(
                roles=[Atspi.Role.TEXT],
                states=[Atspi.StateType.EDITABLE, Atspi.StateType.SINGLE_LINE],
                state_match_type=Atspi.CollectionMatchType.ALL,
            )
            return AXCollection.get_first_match(obj, rule) is not None
        return bool(AXUtilitiesObject.find_descendant(obj, AXUtilitiesRole.is_text_input))

    @staticmethod
    def is_editbar(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the editbar role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.EDITBAR

    @staticmethod
    def is_embedded(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the embedded role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.EMBEDDED

    @staticmethod
    def is_entry(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the entry role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.ENTRY

    @staticmethod
    def is_extended(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the extended role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.EXTENDED

    @staticmethod
    def is_feed(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the feed role"""

        return "feed" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_feed_article(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the article role and descends from a feed."""

        if not AXUtilitiesRole.is_article(obj, role):
            return False

        return AXUtilitiesObject.find_ancestor(obj, AXUtilitiesRole.is_feed) is not None

    @staticmethod
    def is_figure(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the figure role or tag."""

        return (
            "figure" in AXUtilitiesRole._get_xml_roles(obj)
            or AXUtilitiesRole._get_tag(obj) == "figure"
        )

    @staticmethod
    def is_file_chooser(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the file chooser role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.FILE_CHOOSER

    @staticmethod
    def is_filler(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the filler role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.FILLER

    @staticmethod
    def is_focus_traversable(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the focus traversable role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.FOCUS_TRAVERSABLE

    @staticmethod
    def is_font_chooser(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the font chooser role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.FONT_CHOOSER

    @staticmethod
    def is_footer(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the footer role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.FOOTER

    @staticmethod
    def is_footnote(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the footnote role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.FOOTNOTE

    @staticmethod
    def is_form(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the form role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.FORM

    @staticmethod
    def is_frame(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the frame role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.FRAME

    @staticmethod
    def is_glass_pane(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the glass pane role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.GLASS_PANE

    @staticmethod
    def is_gui_list(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the list role but contains UI rather than static text."""

        if not AXUtilitiesRole.is_list(obj, role):
            return False

        return AXObject.get_toolkit_name(obj) == "gtk"

    @staticmethod
    def is_grid(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the grid role."""

        if not AXUtilitiesRole.is_table(obj, role):
            return False

        return "grid" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_grid_cell(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the gridcell role or the cell role and is in a grid."""

        if not AXUtilitiesRole.is_table_cell(obj, role):
            return False

        roles = AXUtilitiesRole._get_xml_roles(obj)
        if "gridcell" in roles:
            return True
        if "cell" in roles:
            return AXUtilitiesObject.find_ancestor(obj, AXUtilitiesRole.is_grid) is not None
        return False

    @staticmethod
    def is_group(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj is an ARIA group."""

        return "group" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_grouping(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the grouping role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.GROUPING

    @staticmethod
    def is_header(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the header role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.HEADER

    @staticmethod
    def is_heading(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the heading role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.HEADING

    @staticmethod
    def is_html_container(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the html container role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.HTML_CONTAINER

    @staticmethod
    def is_horizontal_scrollbar(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj is a horizontal scrollbar"""

        return AXUtilitiesRole.is_scroll_bar(obj, role) and AXObject.has_state(
            obj,
            Atspi.StateType.HORIZONTAL,
        )

    @staticmethod
    def is_horizontal_separator(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj is a horizontal separator"""

        return AXUtilitiesRole.is_separator(obj, role) and AXObject.has_state(
            obj,
            Atspi.StateType.HORIZONTAL,
        )

    @staticmethod
    def is_horizontal_slider(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj is a horizontal slider"""

        return AXUtilitiesRole.is_slider(obj, role) and AXObject.has_state(
            obj,
            Atspi.StateType.HORIZONTAL,
        )

    @staticmethod
    def is_icon(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the icon role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.ICON

    @staticmethod
    def is_icon_or_canvas(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the icon or canvas role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role in [Atspi.Role.ICON, Atspi.Role.CANVAS]

    @staticmethod
    def is_image(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the image role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.IMAGE

    @staticmethod
    def is_image_or_canvas(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the image or canvas role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role in [Atspi.Role.IMAGE, Atspi.Role.CANVAS]

    @staticmethod
    def is_image_map(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the image map role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.IMAGE_MAP

    @staticmethod
    def is_info_bar(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the info bar role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.INFO_BAR

    @staticmethod
    def is_inline_internal_frame(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the internal frame role and is inline."""

        if not AXUtilitiesRole.is_internal_frame(obj, role):
            return False

        return "inline" in AXUtilitiesRole._get_display_style(obj)

    @staticmethod
    def is_inline_suggestion(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the suggestion role and is inline."""

        if not AXUtilitiesRole.is_suggestion(obj, role):
            return False

        return "inline" in AXUtilitiesRole._get_display_style(obj)

    @staticmethod
    def is_inline_element(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj is an inline element rather than block-level."""

        # Chromium exposes inline MathML as display=math; the block form is "block math".
        display = AXUtilitiesRole._get_display_style(obj)
        if "inline" in display or display == "math":
            return True

        # A link is inline-level content; a flex box whose children are presentational is an
        # atomic widget (button, tab, slider) flexing its own icon/label. Both are inline-level
        # regardless of CSS display.
        if AXUtilitiesRole.is_link(obj, role):
            return True

        if "flex" in display and AXUtilitiesRole.children_are_presentational(obj):
            return True

        # A flex item is positioned by its container, not in normal block flow, so it is
        # inline-level for line grouping. (Grid items are excluded: their 2D layout keeps them
        # on separate lines.)
        parent = AXObject.get_parent(obj)
        return parent is not None and "flex" in AXUtilitiesRole._get_display_style(parent)

    @staticmethod
    def is_input_method_window(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the input method window role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.INPUT_METHOD_WINDOW

    @staticmethod
    def is_internal_frame(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the internal frame role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.INTERNAL_FRAME or AXUtilitiesRole._get_tag(obj) == "iframe"

    @staticmethod
    def is_invalid_role(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the invalid role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.INVALID

    @staticmethod
    def is_label(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the label role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.LABEL

    @staticmethod
    def is_label_or_caption(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the label or caption role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role in [Atspi.Role.LABEL, Atspi.Role.CAPTION]

    @staticmethod
    def is_landmark(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the landmark role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.LANDMARK

    @staticmethod
    def is_landmark_banner(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the banner landmark role"""

        return "banner" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_landmark_complementary(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the complementary landmark role"""

        return "complementary" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_landmark_contentinfo(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the contentinfo landmark role"""

        return "contentinfo" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_landmark_form(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the form landmark role"""

        return "form" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_landmark_main(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the main landmark role"""

        return "main" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_landmark_navigation(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the navigation landmark role"""

        return "navigation" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_landmark_region(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the region landmark role"""

        return "region" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_landmark_search(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the search landmark role"""

        return "search" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_landmark_without_type(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the landmark role but no type"""

        if not AXUtilitiesRole.is_landmark(obj, role):
            return False

        roles = AXUtilitiesRole._get_xml_roles(obj)
        return not roles

    @staticmethod
    def is_large_container(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has a large container role"""

        if role is None:
            role = AXObject.get_role(obj)

        return role in AXUtilitiesRole.get_large_container_roles()

    @staticmethod
    def is_layered_pane(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the layered pane role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.LAYERED_PANE

    @staticmethod
    def is_level_bar(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the level bar role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.LEVEL_BAR

    @staticmethod
    def is_link(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the link role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.LINK

    @staticmethod
    def is_list(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the list role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.LIST

    @staticmethod
    def is_list_box(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the list box role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.LIST_BOX

    @staticmethod
    def is_list_box_item(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj is an item in a list box"""

        if not AXUtilitiesRole.is_list_item(obj, role):
            return False
        return AXUtilitiesObject.find_ancestor(obj, AXUtilitiesRole.is_list_box) is not None

    @staticmethod
    def is_list_item(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the list item role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.LIST_ITEM

    @staticmethod
    def is_log(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the log role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.LOG

    @staticmethod
    def is_live_region(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj is a live region."""

        attrs = AXObject.get_attributes_dict(obj, use_cache=False)
        return "container-live" in attrs and attrs.get("container-live") in ["polite", "assertive"]

    @staticmethod
    def is_mark(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the mark role"""

        if role is None:
            role = AXObject.get_role(obj)
        return (
            role == Atspi.Role.MARK
            or "mark" in AXUtilitiesRole._get_xml_roles(obj)
            or AXUtilitiesRole._get_tag(obj) == "mark"
        )

    @staticmethod
    def is_marquee(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the marquee role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.MARQUEE

    @staticmethod
    def is_math(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the math role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.MATH

    @staticmethod
    def is_math_enclose(obj: Atspi.Accessible) -> bool:
        """Returns True if obj has the math enclose role/tag"""

        return AXUtilitiesRole._get_tag(obj) == "menclose"

    @staticmethod
    def is_math_fenced(obj: Atspi.Accessible) -> bool:
        """Returns True if obj has the math fenced role/tag"""

        return AXUtilitiesRole._get_tag(obj) == "mfenced"

    @staticmethod
    def is_math_fraction(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the math fraction role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.MATH_FRACTION

    @staticmethod
    def is_math_fraction_without_bar(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the math fraction role and lacks the fraction bar"""

        if not AXUtilitiesRole.is_math_fraction(obj, role):
            return False

        line_thickness = AXObject.get_attribute(obj, "linethickness")
        if not line_thickness:
            return False

        return all(not (char.isnumeric() and char != "0") for char in line_thickness)

    @staticmethod
    def is_math_layout_only(obj: Atspi.Accessible) -> bool:
        """Returns True if obj has a layout-only math role"""

        return AXUtilitiesRole._get_tag(obj) in [
            "mrow",
            "mstyle",
            "merror",
            "mpadded",
            "none",
            "semantics",
        ]

    @staticmethod
    def is_math_multi_script(obj: Atspi.Accessible) -> bool:
        """Returns True if obj has the math multi-scripts role/tag"""

        return AXUtilitiesRole._get_tag(obj) == "mmultiscripts"

    @staticmethod
    def is_math_related(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has a math-related role or is a text leaf inside math."""

        if obj is None:
            return False

        if role is None:
            role = AXObject.get_role(obj)
        if role in [Atspi.Role.MATH, Atspi.Role.MATH_FRACTION, Atspi.Role.MATH_ROOT]:
            return True

        if AXUtilitiesRole.is_terminal(obj, role):
            return False

        if tag := AXUtilitiesRole._get_tag(obj):
            return tag in AXUtilitiesRole._MATH_TAGS

        return AXUtilitiesRole.is_math_related(AXObject.get_parent(obj))

    @staticmethod
    def is_math_root(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the math root role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.MATH_ROOT

    @staticmethod
    def is_math_square_root(obj: Atspi.Accessible) -> bool:
        """Returns True if obj has the math root role/tag"""

        return AXUtilitiesRole._get_tag(obj) == "msqrt"

    @staticmethod
    def is_math_sub_or_super_script(obj: Atspi.Accessible) -> bool:
        """Returns True if obj has the math subscript or superscript role/tag"""

        return AXUtilitiesRole._get_tag(obj) in ["msub", "msup", "msubsup"]

    @staticmethod
    def is_math_table(obj: Atspi.Accessible) -> bool:
        """Returns True if obj has the math table role/tag"""

        return AXUtilitiesRole._get_tag(obj) == "mtable"

    @staticmethod
    def is_math_table_row(obj: Atspi.Accessible) -> bool:
        """Returns True if obj has the math table row role/tag"""

        return AXUtilitiesRole._get_tag(obj) in ["mtr", "mlabeledtr"]

    @staticmethod
    def is_math_token(obj: Atspi.Accessible) -> bool:
        """Returns True if obj has a math token role/tag"""

        return AXUtilitiesRole._get_tag(obj) in ["mi", "mn", "mo", "mtext", "ms", "mspace"]

    @staticmethod
    def is_math_under_or_over_script(obj: Atspi.Accessible) -> bool:
        """Returns True if obj has the math under-script or over-script role/tag"""

        return AXUtilitiesRole._get_tag(obj) in ["mover", "munder", "munderover"]

    @staticmethod
    def is_menu(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the menu role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.MENU

    @staticmethod
    def is_menu_bar(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the menubar role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.MENU_BAR

    @staticmethod
    def is_menu_item(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the menu item role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.MENU_ITEM

    @staticmethod
    def is_menu_item_of_any_kind(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has any menu item role"""

        roles = AXUtilitiesRole.get_menu_item_roles()
        if role is None:
            role = AXObject.get_role(obj)
        return role in roles

    @staticmethod
    def is_menu_related(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has any menu-related role"""

        roles = AXUtilitiesRole.get_menu_related_roles()
        if role is None:
            role = AXObject.get_role(obj)
        return role in roles

    @staticmethod
    def is_modal_dialog(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the alert or dialog role and modal state"""

        return AXUtilitiesRole.is_dialog_or_alert(obj, role) and AXObject.has_state(
            obj,
            Atspi.StateType.MODAL,
        )

    @staticmethod
    def is_multi_line_entry(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the entry role and multiline state"""

        return AXUtilitiesRole.is_entry(obj, role) and AXObject.has_state(
            obj,
            Atspi.StateType.MULTI_LINE,
        )

    @staticmethod
    def is_notification(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the notification role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.NOTIFICATION

    @staticmethod
    def is_option_pane(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the option pane role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.OPTION_PANE

    @staticmethod
    def is_page(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the page role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.PAGE

    @staticmethod
    def is_page_tab(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the page tab role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.PAGE_TAB

    @staticmethod
    def is_page_tab_list(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the page tab list role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.PAGE_TAB_LIST

    @staticmethod
    def is_page_tab_list_related(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the page tab or page tab list role"""

        roles = [Atspi.Role.PAGE_TAB_LIST, Atspi.Role.PAGE_TAB]
        if role is None:
            role = AXObject.get_role(obj)
        return role in roles

    @staticmethod
    def is_panel(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the panel role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.PANEL

    @staticmethod
    def is_paragraph(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the paragraph role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.PARAGRAPH

    @staticmethod
    def is_password_text(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the password text role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.PASSWORD_TEXT

    @staticmethod
    def is_popup_menu(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the popup menu role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.POPUP_MENU

    @staticmethod
    def is_progress_bar(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the progress bar role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.PROGRESS_BAR

    @staticmethod
    def is_push_button(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the push button role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.BUTTON

    @staticmethod
    def is_push_button_menu(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the push button menu role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.PUSH_BUTTON_MENU

    @staticmethod
    def is_radio_button(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the radio button role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.RADIO_BUTTON

    @staticmethod
    def is_radio_menu_item(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the radio menu item role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.RADIO_MENU_ITEM

    @staticmethod
    def is_rating(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the rating role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.RATING

    @staticmethod
    def is_redundant_object_role(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the redundant object role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.REDUNDANT_OBJECT

    @staticmethod
    def is_root_pane(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the root pane role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.ROOT_PANE

    @staticmethod
    def is_row_header(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the row header role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.ROW_HEADER

    @staticmethod
    def is_ruler(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the ruler role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.RULER

    @staticmethod
    def is_scroll_bar(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the scrollbar role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.SCROLL_BAR

    @staticmethod
    def is_scroll_pane(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the scroll pane role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.SCROLL_PANE

    @staticmethod
    def is_section(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the section role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.SECTION

    @staticmethod
    def is_separator(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the separator role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.SEPARATOR

    @staticmethod
    def is_single_line_autocomplete_entry(
        obj: Atspi.Accessible,
        role: Atspi.Role | None = None,
    ) -> bool:
        """Returns True if obj has the entry role and single-line state"""

        if not AXUtilitiesRole.is_single_line_entry(obj, role):
            return False

        return AXUtilitiesState.supports_autocompletion(obj)

    @staticmethod
    def is_single_line_entry(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the entry role and the single-line state"""

        if not AXUtilitiesState.is_single_line(obj):
            return False
        if AXUtilitiesRole.is_entry(obj, role):
            return True
        if AXUtilitiesRole.is_text(obj, role):
            return AXUtilitiesState.is_editable(obj)
        return False

    @staticmethod
    def is_slider(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the slider role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.SLIDER

    @staticmethod
    def is_spin_button(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the spin button role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.SPIN_BUTTON

    @staticmethod
    def is_split_pane(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the split pane role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.SPLIT_PANE

    @staticmethod
    def is_static(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the static role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.STATIC

    @staticmethod
    def is_status_bar(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the statusbar role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.STATUS_BAR

    @staticmethod
    def is_subscript(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the subscript role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.SUBSCRIPT

    @staticmethod
    def is_subscript_or_superscript(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the subscript or superscript role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role in [Atspi.Role.SUBSCRIPT, Atspi.Role.SUPERSCRIPT]

    @staticmethod
    def is_suggestion(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the suggestion role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.SUGGESTION or "suggestion" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_superscript(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the superscript role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.SUPERSCRIPT

    @staticmethod
    def is_svg(obj: Atspi.Accessible) -> bool:
        """Returns True if obj is an svg."""

        return AXUtilitiesRole._get_tag(obj) == "svg"

    @staticmethod
    def is_switch(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the switch role."""

        if role is None:
            role = AXObject.get_role(obj)

        if role == Atspi.Role.SWITCH:
            return True

        return "switch" in AXUtilitiesRole._get_xml_roles(obj)

    @staticmethod
    def is_table(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the table role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.TABLE

    @staticmethod
    def is_table_cell(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the table cell role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.TABLE_CELL

    @staticmethod
    def is_table_cell_or_header(
        obj: Atspi.Accessible,
        role: Atspi.Role | None = None,
        include_display: bool = False,
    ) -> bool:
        """Returns True if obj is a table cell/header by role, or CSS display if include_display."""

        roles = AXUtilitiesRole.get_table_cell_roles()
        if role is None:
            role = AXObject.get_role(obj)
        if role in roles:
            return True
        return include_display and AXUtilitiesRole._get_display_style(obj) == "table-cell"

    @staticmethod
    def is_table_column_header(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the table column header role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.TABLE_COLUMN_HEADER

    @staticmethod
    def is_table_header(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has a table header related role"""

        roles = AXUtilitiesRole.get_table_header_roles()
        if role is None:
            role = AXObject.get_role(obj)
        return role in roles

    @staticmethod
    def is_table_related(
        obj: Atspi.Accessible,
        role: Atspi.Role | None = None,
        include_caption: bool = False,
    ) -> bool:
        """Returns True if obj has a table-related role"""

        roles = AXUtilitiesRole.get_table_related_roles(include_caption)
        if role is None:
            role = AXObject.get_role(obj)
        return role in roles

    @staticmethod
    def is_table_row(
        obj: Atspi.Accessible,
        role: Atspi.Role | None = None,
        include_display: bool = False,
    ) -> bool:
        """Returns True if obj is a table row by role, or CSS display if include_display."""

        if role is None:
            role = AXObject.get_role(obj)
        if role == Atspi.Role.TABLE_ROW:
            return True
        return include_display and AXUtilitiesRole._get_display_style(obj) == "table-row"

    @staticmethod
    def is_table_row_header(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the table row header role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.TABLE_ROW_HEADER

    @staticmethod
    def is_tearoff_menu_item(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the tearoff menu item role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.TEAROFF_MENU_ITEM

    @staticmethod
    def is_terminal(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the terminal role"""
        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.TERMINAL

    @staticmethod
    def is_text(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the text role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.TEXT

    @staticmethod
    def is_text_input(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has any role associated with textual input"""

        roles = [Atspi.Role.ENTRY, Atspi.Role.PASSWORD_TEXT, Atspi.Role.SPIN_BUTTON]
        if role is None:
            role = AXObject.get_role(obj)
        if role in roles:
            return True
        if role == Atspi.Role.TEXT:
            states = AXObject.get_state_set(obj)
            return AXUtilitiesState.is_editable(obj, states) and AXUtilitiesState.is_single_line(
                obj, states
            )
        return bool(AXUtilitiesRole.is_editable_combo_box(obj))

    @staticmethod
    def is_text_input_date(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj is a date text input"""

        if not AXUtilitiesRole.is_text_input(obj, role):
            return False

        attrs = AXObject.get_attributes_dict(obj)
        return attrs.get("text-input-type") == "date"

    @staticmethod
    def is_text_input_email(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj is an email text input"""

        if not AXUtilitiesRole.is_text_input(obj, role):
            return False

        attrs = AXObject.get_attributes_dict(obj)
        return attrs.get("text-input-type") == "email"

    @staticmethod
    def is_text_input_number(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj is a numeric text input"""

        if not AXUtilitiesRole.is_text_input(obj, role):
            return False

        attrs = AXObject.get_attributes_dict(obj)
        return attrs.get("text-input-type") == "number"

    @staticmethod
    def is_text_input_search(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj is a telephone text input"""

        if not AXUtilitiesRole.is_text_input(obj, role):
            return False

        attrs = AXObject.get_attributes_dict(obj)
        xml_roles = AXUtilitiesRole._get_xml_roles(obj)
        if attrs.get("text-input-type") == "search" or "searchbox" in xml_roles:
            return True

        ax_id = AXObject.get_accessible_id(obj) or ""
        if ax_id:
            return "search" in ax_id.lower() or "find" in ax_id.lower()

        child = AXObject.get_child(obj, 0)
        if AXUtilitiesRole.is_icon(child) or AXUtilitiesRole.is_image(child):
            child_id = AXObject.get_accessible_id(child) or ""
            if "search" in child_id.lower() or "find" in child_id.lower():
                return True
            # Some toolkits don't localize the symbolic icon names, so it's worth a try.
            child_name = AXObject.get_name(child).lower()
            return "search" in child_name or "find" in child_name

        return False

    @staticmethod
    def is_text_input_telephone(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj is a telephone text input"""

        if not AXUtilitiesRole.is_text_input(obj, role):
            return False

        attrs = AXObject.get_attributes_dict(obj)
        return attrs.get("text-input-type") == "telephone"

    @staticmethod
    def is_text_input_time(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj is a time text input"""

        if not AXUtilitiesRole.is_text_input(obj, role):
            return False

        attrs = AXObject.get_attributes_dict(obj)
        return attrs.get("text-input-type") == "time"

    @staticmethod
    def is_text_input_url(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj is a url text input"""

        if not AXUtilitiesRole.is_text_input(obj, role):
            return False

        attrs = AXObject.get_attributes_dict(obj)
        return attrs.get("text-input-type") == "url"

    @staticmethod
    def is_text_input_week(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj is a week text input"""

        if not AXUtilitiesRole.is_text_input(obj, role):
            return False

        attrs = AXObject.get_attributes_dict(obj)
        return attrs.get("text-input-type") == "week"

    @staticmethod
    def is_time(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the time role"""

        return (
            "time" in AXUtilitiesRole._get_xml_roles(obj) or AXUtilitiesRole._get_tag(obj) == "time"
        )

    @staticmethod
    def is_timer(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the timer role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.TIMER

    @staticmethod
    def is_title_bar(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the titlebar role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.TITLE_BAR

    @staticmethod
    def is_toggle_button(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the toggle button role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.TOGGLE_BUTTON

    @staticmethod
    def is_tool_bar(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the toolbar role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.TOOL_BAR

    @staticmethod
    def is_tool_tip(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the tooltip role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.TOOL_TIP

    @staticmethod
    def is_tree(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the tree role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.TREE

    @staticmethod
    def is_tree_or_tree_table(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the tree or tree table role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role in [Atspi.Role.TREE, Atspi.Role.TREE_TABLE]

    @staticmethod
    def is_tree_related(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has a tree-related role"""

        roles = [Atspi.Role.TREE, Atspi.Role.TREE_ITEM, Atspi.Role.TREE_TABLE]
        if role is None:
            role = AXObject.get_role(obj)
        return role in roles

    @staticmethod
    def is_tree_item(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the tree item role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.TREE_ITEM

    @staticmethod
    def is_tree_table(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the tree table role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.TREE_TABLE

    @staticmethod
    def is_unknown(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the unknown role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.UNKNOWN

    @staticmethod
    def is_unknown_or_redundant(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the unknown or redundant object role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role in [Atspi.Role.UNKNOWN, Atspi.Role.REDUNDANT_OBJECT]

    @staticmethod
    def is_vertical_scrollbar(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj is a vertical scrollbar"""

        return AXUtilitiesRole.is_scroll_bar(obj, role) and AXObject.has_state(
            obj,
            Atspi.StateType.VERTICAL,
        )

    @staticmethod
    def is_vertical_separator(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj is a vertical separator"""

        return AXUtilitiesRole.is_separator(obj, role) and AXObject.has_state(
            obj,
            Atspi.StateType.VERTICAL,
        )

    @staticmethod
    def is_vertical_slider(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj is a vertical slider"""

        return AXUtilitiesRole.is_slider(obj, role) and AXObject.has_state(
            obj,
            Atspi.StateType.VERTICAL,
        )

    @staticmethod
    def is_video(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the video role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.VIDEO

    @staticmethod
    def is_viewport(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the viewport role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.VIEWPORT

    @staticmethod
    def is_web_element(obj: Atspi.Accessible, exclude_pseudo_elements: bool = True) -> bool:
        """Returns True if obj is a web element"""

        tag = AXUtilitiesRole._get_tag(obj)
        if not tag:
            return False
        if not exclude_pseudo_elements:
            return True
        exclude = ["::before", "::after", "::marker"]
        return tag not in exclude

    @staticmethod
    def is_web_element_custom(obj: Atspi.Accessible) -> bool:
        """Returns True if obj is a custom web element"""

        tag = AXUtilitiesRole._get_tag(obj)
        return tag is not None and "-" in tag

    @staticmethod
    def is_widget(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has a widget role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role in AXUtilitiesRole.get_widget_roles()

    @staticmethod
    def is_widget_controlled_by_line_navigation(
        obj: Atspi.Accessible,
        role: Atspi.Role | None = None,
    ) -> bool:
        """Returns True if obj is a widget controlled by line navigation"""

        state_set = AXObject.get_state_set(obj)
        if AXUtilitiesState.is_multi_line(obj, state_set):
            return False

        if role is None:
            role = AXObject.get_role(obj)

        roles = [
            Atspi.Role.COMBO_BOX,
            Atspi.Role.LIST_BOX,
            Atspi.Role.MENU,
            Atspi.Role.SPIN_BUTTON,
            Atspi.Role.TREE,
            Atspi.Role.TREE_TABLE,
        ]
        if role in roles:
            return True

        if AXUtilitiesState.is_editable(obj, state_set) or AXUtilitiesState.is_selectable(
            obj, state_set
        ):
            return (
                AXUtilitiesObject.find_ancestor(obj, lambda x: AXObject.get_role(x) in roles)
                is not None
            )

        if not AXUtilitiesState.is_vertical(obj, state_set):
            return False

        return role in [
            Atspi.Role.SCROLL_BAR,
            Atspi.Role.SEPARATOR,
            Atspi.Role.SLIDER,
            Atspi.Role.SPLIT_PANE,
        ]

    @staticmethod
    def is_window(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool:
        """Returns True if obj has the window role"""

        if role is None:
            role = AXObject.get_role(obj)
        return role == Atspi.Role.WINDOW
