/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "AccessibleWrap.h" #include "LocalAccessible-inl.h" #include "AccAttributes.h" #include "ApplicationAccessibleWrap.h" #include "InterfaceInitFuncs.h" #include "nsAccUtils.h" #include "mozilla/a11y/PDocAccessible.h" #include "OuterDocAccessible.h" #include "RemoteAccessible.h" #include "DocAccessibleParent.h" #include "RootAccessible.h" #include "mozilla/a11y/TableAccessible.h" #include "mozilla/a11y/TableCellAccessible.h" #include "nsMai.h" #include "nsMaiHyperlink.h" #include "nsString.h" #include "nsStateMap.h" #include "mozilla/a11y/Platform.h" #include "Relation.h" #include "RootAccessible.h" #include "States.h" #include "nsISimpleEnumerator.h" #include "mozilla/ArrayUtils.h" #include "mozilla/Sprintf.h" #include "nsAccessibilityService.h" #include "nsComponentManagerUtils.h" using namespace mozilla; using namespace mozilla::a11y; MaiAtkObject::EAvailableAtkSignals MaiAtkObject::gAvailableAtkSignals = eUnknown; // defined in ApplicationAccessibleWrap.cpp extern "C" GType g_atk_hyperlink_impl_type; /* MaiAtkObject */ enum { ACTIVATE, CREATE, DEACTIVATE, DESTROY, MAXIMIZE, MINIMIZE, RESIZE, RESTORE, LAST_SIGNAL }; enum MaiInterfaceType { MAI_INTERFACE_COMPONENT, /* 0 */ MAI_INTERFACE_ACTION, MAI_INTERFACE_VALUE, MAI_INTERFACE_EDITABLE_TEXT, MAI_INTERFACE_HYPERTEXT, MAI_INTERFACE_HYPERLINK_IMPL, MAI_INTERFACE_SELECTION, MAI_INTERFACE_TABLE, MAI_INTERFACE_TEXT, MAI_INTERFACE_DOCUMENT, MAI_INTERFACE_IMAGE, /* 10 */ MAI_INTERFACE_TABLE_CELL }; static GType GetAtkTypeForMai(MaiInterfaceType type) { switch (type) { case MAI_INTERFACE_COMPONENT: return ATK_TYPE_COMPONENT; case MAI_INTERFACE_ACTION: return ATK_TYPE_ACTION; case MAI_INTERFACE_VALUE: return ATK_TYPE_VALUE; case MAI_INTERFACE_EDITABLE_TEXT: return ATK_TYPE_EDITABLE_TEXT; case MAI_INTERFACE_HYPERTEXT: return ATK_TYPE_HYPERTEXT; case MAI_INTERFACE_HYPERLINK_IMPL: return g_atk_hyperlink_impl_type; case MAI_INTERFACE_SELECTION: return ATK_TYPE_SELECTION; case MAI_INTERFACE_TABLE: return ATK_TYPE_TABLE; case MAI_INTERFACE_TEXT: return ATK_TYPE_TEXT; case MAI_INTERFACE_DOCUMENT: return ATK_TYPE_DOCUMENT; case MAI_INTERFACE_IMAGE: return ATK_TYPE_IMAGE; case MAI_INTERFACE_TABLE_CELL: MOZ_ASSERT(false); } return G_TYPE_INVALID; } #define NON_USER_EVENT ":system" // The atk interfaces we can expose without checking what version of ATK we are // dealing with. At the moment AtkTableCell is the only interface we can't // always expose. static const GInterfaceInfo atk_if_infos[] = { {(GInterfaceInitFunc)componentInterfaceInitCB, (GInterfaceFinalizeFunc) nullptr, nullptr}, {(GInterfaceInitFunc)actionInterfaceInitCB, (GInterfaceFinalizeFunc) nullptr, nullptr}, {(GInterfaceInitFunc)valueInterfaceInitCB, (GInterfaceFinalizeFunc) nullptr, nullptr}, {(GInterfaceInitFunc)editableTextInterfaceInitCB, (GInterfaceFinalizeFunc) nullptr, nullptr}, {(GInterfaceInitFunc)hypertextInterfaceInitCB, (GInterfaceFinalizeFunc) nullptr, nullptr}, {(GInterfaceInitFunc)hyperlinkImplInterfaceInitCB, (GInterfaceFinalizeFunc) nullptr, nullptr}, {(GInterfaceInitFunc)selectionInterfaceInitCB, (GInterfaceFinalizeFunc) nullptr, nullptr}, {(GInterfaceInitFunc)tableInterfaceInitCB, (GInterfaceFinalizeFunc) nullptr, nullptr}, {(GInterfaceInitFunc)textInterfaceInitCB, (GInterfaceFinalizeFunc) nullptr, nullptr}, {(GInterfaceInitFunc)documentInterfaceInitCB, (GInterfaceFinalizeFunc) nullptr, nullptr}, {(GInterfaceInitFunc)imageInterfaceInitCB, (GInterfaceFinalizeFunc) nullptr, nullptr}}; static GQuark quark_mai_hyperlink = 0; AtkHyperlink* MaiAtkObject::GetAtkHyperlink() { NS_ASSERTION(quark_mai_hyperlink, "quark_mai_hyperlink not initialized"); MaiHyperlink* maiHyperlink = (MaiHyperlink*)g_object_get_qdata(G_OBJECT(this), quark_mai_hyperlink); if (!maiHyperlink) { maiHyperlink = new MaiHyperlink(acc); g_object_set_qdata(G_OBJECT(this), quark_mai_hyperlink, maiHyperlink); } return maiHyperlink->GetAtkHyperlink(); } void MaiAtkObject::Shutdown() { acc = nullptr; MaiHyperlink* maiHyperlink = (MaiHyperlink*)g_object_get_qdata(G_OBJECT(this), quark_mai_hyperlink); if (maiHyperlink) { delete maiHyperlink; g_object_set_qdata(G_OBJECT(this), quark_mai_hyperlink, nullptr); } } struct MaiAtkObjectClass { AtkObjectClass parent_class; }; static guint mai_atk_object_signals[LAST_SIGNAL] = { 0, }; static void MaybeFireNameChange(AtkObject* aAtkObj, const nsString& aNewName); G_BEGIN_DECLS /* callbacks for MaiAtkObject */ static void classInitCB(AtkObjectClass* aClass); static void initializeCB(AtkObject* aAtkObj, gpointer aData); static void finalizeCB(GObject* aObj); /* callbacks for AtkObject virtual functions */ static const gchar* getNameCB(AtkObject* aAtkObj); /* getDescriptionCB is also used by image interface */ const gchar* getDescriptionCB(AtkObject* aAtkObj); static AtkRole getRoleCB(AtkObject* aAtkObj); static AtkAttributeSet* getAttributesCB(AtkObject* aAtkObj); static const gchar* GetLocaleCB(AtkObject*); static AtkObject* getParentCB(AtkObject* aAtkObj); static gint getChildCountCB(AtkObject* aAtkObj); static AtkObject* refChildCB(AtkObject* aAtkObj, gint aChildIndex); static gint getIndexInParentCB(AtkObject* aAtkObj); static AtkStateSet* refStateSetCB(AtkObject* aAtkObj); static AtkRelationSet* refRelationSetCB(AtkObject* aAtkObj); /* the missing atkobject virtual functions */ /* static AtkLayer getLayerCB(AtkObject *aAtkObj); static gint getMdiZorderCB(AtkObject *aAtkObj); static void SetNameCB(AtkObject *aAtkObj, const gchar *name); static void SetDescriptionCB(AtkObject *aAtkObj, const gchar *description); static void SetParentCB(AtkObject *aAtkObj, AtkObject *parent); static void SetRoleCB(AtkObject *aAtkObj, AtkRole role); static guint ConnectPropertyChangeHandlerCB( AtkObject *aObj, AtkPropertyChangeHandler *handler); static void RemovePropertyChangeHandlerCB( AtkObject *aAtkObj, guint handler_id); static void InitializeCB(AtkObject *aAtkObj, gpointer data); static void ChildrenChangedCB(AtkObject *aAtkObj, guint change_index, gpointer changed_child); static void FocusEventCB(AtkObject *aAtkObj, gboolean focus_in); static void PropertyChangeCB(AtkObject *aAtkObj, AtkPropertyValues *values); static void StateChangeCB(AtkObject *aAtkObj, const gchar *name, gboolean state_set); static void VisibleDataChangedCB(AtkObject *aAtkObj); */ G_END_DECLS static GType GetMaiAtkType(uint16_t interfacesBits); static const char* GetUniqueMaiAtkTypeName(uint16_t interfacesBits); static gpointer parent_class = nullptr; GType mai_atk_object_get_type(void) { static GType type = 0; if (!type) { static const GTypeInfo tinfo = { sizeof(MaiAtkObjectClass), (GBaseInitFunc) nullptr, (GBaseFinalizeFunc) nullptr, (GClassInitFunc)classInitCB, (GClassFinalizeFunc) nullptr, nullptr, /* class data */ sizeof(MaiAtkObject), /* instance size */ 0, /* nb preallocs */ (GInstanceInitFunc) nullptr, nullptr /* value table */ }; type = g_type_register_static(ATK_TYPE_OBJECT, "MaiAtkObject", &tinfo, GTypeFlags(0)); quark_mai_hyperlink = g_quark_from_static_string("MaiHyperlink"); } return type; } AccessibleWrap::AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc) : LocalAccessible(aContent, aDoc), mAtkObject(nullptr) {} AccessibleWrap::~AccessibleWrap() { NS_ASSERTION(!mAtkObject, "ShutdownAtkObject() is not called"); } void AccessibleWrap::ShutdownAtkObject() { if (!mAtkObject) return; NS_ASSERTION(IS_MAI_OBJECT(mAtkObject), "wrong type of atk object"); if (IS_MAI_OBJECT(mAtkObject)) MAI_ATK_OBJECT(mAtkObject)->Shutdown(); g_object_unref(mAtkObject); mAtkObject = nullptr; } void AccessibleWrap::Shutdown() { ShutdownAtkObject(); LocalAccessible::Shutdown(); } void AccessibleWrap::GetNativeInterface(void** aOutAccessible) { *aOutAccessible = nullptr; if (!mAtkObject) { if (IsDefunct() || IsText()) { // We don't create ATK objects for node which has been shutdown or // plain text leaves return; } GType type = GetMaiAtkType(CreateMaiInterfaces()); if (!type) return; mAtkObject = reinterpret_cast(g_object_new(type, nullptr)); if (!mAtkObject) return; atk_object_initialize(mAtkObject, static_cast(this)); mAtkObject->role = ATK_ROLE_INVALID; mAtkObject->layer = ATK_LAYER_INVALID; } *aOutAccessible = mAtkObject; } AtkObject* AccessibleWrap::GetAtkObject(void) { void* atkObj = nullptr; GetNativeInterface(&atkObj); return static_cast(atkObj); } // Get AtkObject from LocalAccessible interface /* static */ AtkObject* AccessibleWrap::GetAtkObject(LocalAccessible* acc) { void* atkObjPtr = nullptr; acc->GetNativeInterface(&atkObjPtr); return atkObjPtr ? ATK_OBJECT(atkObjPtr) : nullptr; } /* private */ uint16_t AccessibleWrap::CreateMaiInterfaces(void) { uint16_t interfacesBits = 0; // The Component interface is supported by all accessibles. interfacesBits |= 1 << MAI_INTERFACE_COMPONENT; // Add Action interface if the action count is more than zero. if (ActionCount() > 0) interfacesBits |= 1 << MAI_INTERFACE_ACTION; // Text, Editabletext, and Hypertext interface. HyperTextAccessible* hyperText = AsHyperText(); if (hyperText && hyperText->IsTextRole()) { interfacesBits |= 1 << MAI_INTERFACE_TEXT; interfacesBits |= 1 << MAI_INTERFACE_EDITABLE_TEXT; if (!nsAccUtils::MustPrune(this)) { interfacesBits |= 1 << MAI_INTERFACE_HYPERTEXT; } } // Value interface. if (HasNumericValue()) interfacesBits |= 1 << MAI_INTERFACE_VALUE; // Document interface. if (IsDoc()) interfacesBits |= 1 << MAI_INTERFACE_DOCUMENT; if (IsImage()) interfacesBits |= 1 << MAI_INTERFACE_IMAGE; // HyperLink interface. if (IsLink()) interfacesBits |= 1 << MAI_INTERFACE_HYPERLINK_IMPL; if (!nsAccUtils::MustPrune(this)) { // These interfaces require children // Table interface. if (AsTable()) interfacesBits |= 1 << MAI_INTERFACE_TABLE; if (AsTableCell()) interfacesBits |= 1 << MAI_INTERFACE_TABLE_CELL; // Selection interface. if (IsSelect()) { interfacesBits |= 1 << MAI_INTERFACE_SELECTION; } } return interfacesBits; } static GType GetMaiAtkType(uint16_t interfacesBits) { GType type; static const GTypeInfo tinfo = { sizeof(MaiAtkObjectClass), (GBaseInitFunc) nullptr, (GBaseFinalizeFunc) nullptr, (GClassInitFunc) nullptr, (GClassFinalizeFunc) nullptr, nullptr, /* class data */ sizeof(MaiAtkObject), /* instance size */ 0, /* nb preallocs */ (GInstanceInitFunc) nullptr, nullptr /* value table */ }; /* * The members we use to register GTypes are GetAtkTypeForMai * and atk_if_infos, which are constant values to each MaiInterface * So we can reuse the registered GType when having * the same MaiInterface types. */ const char* atkTypeName = GetUniqueMaiAtkTypeName(interfacesBits); type = g_type_from_name(atkTypeName); if (type) { return type; } /* * gobject limits the number of types that can directly derive from any * given object type to 4095. */ static uint16_t typeRegCount = 0; if (typeRegCount++ >= 4095) { return G_TYPE_INVALID; } type = g_type_register_static(MAI_TYPE_ATK_OBJECT, atkTypeName, &tinfo, GTypeFlags(0)); for (uint32_t index = 0; index < ArrayLength(atk_if_infos); index++) { if (interfacesBits & (1 << index)) { g_type_add_interface_static(type, GetAtkTypeForMai((MaiInterfaceType)index), &atk_if_infos[index]); } } // Special case AtkTableCell so we can check what version of Atk we are // dealing with. if (IsAtkVersionAtLeast(2, 12) && (interfacesBits & (1 << MAI_INTERFACE_TABLE_CELL))) { const GInterfaceInfo cellInfo = { (GInterfaceInitFunc)tableCellInterfaceInitCB, (GInterfaceFinalizeFunc) nullptr, nullptr}; g_type_add_interface_static(type, gAtkTableCellGetTypeFunc(), &cellInfo); } return type; } static const char* GetUniqueMaiAtkTypeName(uint16_t interfacesBits) { #define MAI_ATK_TYPE_NAME_LEN (30) /* 10+sizeof(uint16_t)*8/4+1 < 30 */ static gchar namePrefix[] = "MaiAtkType"; /* size = 10 */ static gchar name[MAI_ATK_TYPE_NAME_LEN + 1]; SprintfLiteral(name, "%s%x", namePrefix, interfacesBits); name[MAI_ATK_TYPE_NAME_LEN] = '\0'; return name; } bool AccessibleWrap::IsValidObject() { // to ensure we are not shut down return !IsDefunct(); } /* static functions for ATK callbacks */ void classInitCB(AtkObjectClass* aClass) { GObjectClass* gobject_class = G_OBJECT_CLASS(aClass); parent_class = g_type_class_peek_parent(aClass); aClass->get_name = getNameCB; aClass->get_description = getDescriptionCB; aClass->get_parent = getParentCB; aClass->get_n_children = getChildCountCB; aClass->ref_child = refChildCB; aClass->get_index_in_parent = getIndexInParentCB; aClass->get_role = getRoleCB; aClass->get_attributes = getAttributesCB; aClass->get_object_locale = GetLocaleCB; aClass->ref_state_set = refStateSetCB; aClass->ref_relation_set = refRelationSetCB; aClass->initialize = initializeCB; gobject_class->finalize = finalizeCB; mai_atk_object_signals[ACTIVATE] = g_signal_new( "activate", MAI_TYPE_ATK_OBJECT, G_SIGNAL_RUN_LAST, 0, /* default signal handler */ nullptr, nullptr, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); mai_atk_object_signals[CREATE] = g_signal_new( "create", MAI_TYPE_ATK_OBJECT, G_SIGNAL_RUN_LAST, 0, /* default signal handler */ nullptr, nullptr, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); mai_atk_object_signals[DEACTIVATE] = g_signal_new( "deactivate", MAI_TYPE_ATK_OBJECT, G_SIGNAL_RUN_LAST, 0, /* default signal handler */ nullptr, nullptr, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); mai_atk_object_signals[DESTROY] = g_signal_new( "destroy", MAI_TYPE_ATK_OBJECT, G_SIGNAL_RUN_LAST, 0, /* default signal handler */ nullptr, nullptr, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); mai_atk_object_signals[MAXIMIZE] = g_signal_new( "maximize", MAI_TYPE_ATK_OBJECT, G_SIGNAL_RUN_LAST, 0, /* default signal handler */ nullptr, nullptr, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); mai_atk_object_signals[MINIMIZE] = g_signal_new( "minimize", MAI_TYPE_ATK_OBJECT, G_SIGNAL_RUN_LAST, 0, /* default signal handler */ nullptr, nullptr, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); mai_atk_object_signals[RESIZE] = g_signal_new( "resize", MAI_TYPE_ATK_OBJECT, G_SIGNAL_RUN_LAST, 0, /* default signal handler */ nullptr, nullptr, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); mai_atk_object_signals[RESTORE] = g_signal_new( "restore", MAI_TYPE_ATK_OBJECT, G_SIGNAL_RUN_LAST, 0, /* default signal handler */ nullptr, nullptr, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); } void initializeCB(AtkObject* aAtkObj, gpointer aData) { NS_ASSERTION((IS_MAI_OBJECT(aAtkObj)), "Invalid AtkObject"); NS_ASSERTION(aData, "Invalid Data to init AtkObject"); if (!aAtkObj || !aData) return; /* call parent init function */ /* AtkObjectClass has not a "initialize" function now, * maybe it has later */ if (ATK_OBJECT_CLASS(parent_class)->initialize) { ATK_OBJECT_CLASS(parent_class)->initialize(aAtkObj, aData); } /* initialize object */ MAI_ATK_OBJECT(aAtkObj)->acc = static_cast(aData); } void finalizeCB(GObject* aObj) { if (!IS_MAI_OBJECT(aObj)) return; NS_ASSERTION(!MAI_ATK_OBJECT(aObj)->acc, "acc NOT null"); // call parent finalize function // finalize of GObjectClass will unref the accessible parent if has if (G_OBJECT_CLASS(parent_class)->finalize) { G_OBJECT_CLASS(parent_class)->finalize(aObj); } } const gchar* getNameCB(AtkObject* aAtkObj) { nsAutoString name; if (Accessible* acc = GetInternalObj(aAtkObj)) { acc->Name(name); } else { return nullptr; } // XXX Firing an event from here does not seem right MaybeFireNameChange(aAtkObj, name); return aAtkObj->name; } static void MaybeFireNameChange(AtkObject* aAtkObj, const nsString& aNewName) { NS_ConvertUTF16toUTF8 newNameUTF8(aNewName); if (aAtkObj->name && !strcmp(aAtkObj->name, newNameUTF8.get())) return; // Below we duplicate the functionality of atk_object_set_name(), // but without calling atk_object_get_name(). Instead of // atk_object_get_name() we directly access aAtkObj->name. This is because // atk_object_get_name() would call getNameCB() which would call // MaybeFireNameChange() (or atk_object_set_name() before this problem was // fixed) and we would get an infinite recursion. // See http://bugzilla.mozilla.org/733712 // Do not notify for initial name setting. // See bug http://bugzilla.gnome.org/665870 bool notify = !!aAtkObj->name; free(aAtkObj->name); aAtkObj->name = strdup(newNameUTF8.get()); if (notify) g_object_notify(G_OBJECT(aAtkObj), "accessible-name"); } const gchar* getDescriptionCB(AtkObject* aAtkObj) { nsAutoString uniDesc; if (Accessible* acc = GetInternalObj(aAtkObj)) { acc->Description(uniDesc); } else { return nullptr; } NS_ConvertUTF8toUTF16 objDesc(aAtkObj->description); if (!uniDesc.Equals(objDesc)) { atk_object_set_description(aAtkObj, NS_ConvertUTF16toUTF8(uniDesc).get()); } return aAtkObj->description; } AtkRole getRoleCB(AtkObject* aAtkObj) { if (aAtkObj->role != ATK_ROLE_INVALID) return aAtkObj->role; Accessible* acc = GetInternalObj(aAtkObj); if (!acc) { return ATK_ROLE_INVALID; } #ifdef DEBUG if (AccessibleWrap* accWrap = GetAccessibleWrap(aAtkObj)) { NS_ASSERTION(nsAccUtils::IsTextInterfaceSupportCorrect(accWrap), "Does not support Text interface when it should"); } #endif #define ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \ msaaRole, ia2Role, androidClass, iosIsElement, nameRule) \ case roles::geckoRole: \ aAtkObj->role = atkRole; \ break; switch (acc->Role()) { #include "RoleMap.h" default: MOZ_CRASH("Unknown role."); } #undef ROLE if (aAtkObj->role == ATK_ROLE_LIST_BOX && !IsAtkVersionAtLeast(2, 1)) { aAtkObj->role = ATK_ROLE_LIST; } else if (aAtkObj->role == ATK_ROLE_TABLE_ROW && !IsAtkVersionAtLeast(2, 1)) { aAtkObj->role = ATK_ROLE_LIST_ITEM; } else if (aAtkObj->role == ATK_ROLE_MATH && !IsAtkVersionAtLeast(2, 12)) { aAtkObj->role = ATK_ROLE_SECTION; } else if (aAtkObj->role == ATK_ROLE_COMMENT && !IsAtkVersionAtLeast(2, 12)) { aAtkObj->role = ATK_ROLE_SECTION; } else if (aAtkObj->role == ATK_ROLE_LANDMARK && !IsAtkVersionAtLeast(2, 12)) { aAtkObj->role = ATK_ROLE_SECTION; } else if (aAtkObj->role == ATK_ROLE_FOOTNOTE && !IsAtkVersionAtLeast(2, 25, 2)) { aAtkObj->role = ATK_ROLE_SECTION; } else if (aAtkObj->role == ATK_ROLE_STATIC && !IsAtkVersionAtLeast(2, 16)) { aAtkObj->role = ATK_ROLE_TEXT; } else if ((aAtkObj->role == ATK_ROLE_MATH_FRACTION || aAtkObj->role == ATK_ROLE_MATH_ROOT) && !IsAtkVersionAtLeast(2, 16)) { aAtkObj->role = ATK_ROLE_SECTION; } else if (aAtkObj->role == ATK_ROLE_MARK && !IsAtkVersionAtLeast(2, 36)) { aAtkObj->role = ATK_ROLE_TEXT; } else if (aAtkObj->role == ATK_ROLE_SUGGESTION && !IsAtkVersionAtLeast(2, 36)) { aAtkObj->role = ATK_ROLE_SECTION; } else if (aAtkObj->role == ATK_ROLE_COMMENT && !IsAtkVersionAtLeast(2, 36)) { aAtkObj->role = ATK_ROLE_SECTION; } else if ((aAtkObj->role == ATK_ROLE_CONTENT_DELETION || aAtkObj->role == ATK_ROLE_CONTENT_INSERTION) && !IsAtkVersionAtLeast(2, 34)) { aAtkObj->role = ATK_ROLE_SECTION; } return aAtkObj->role; } static AtkAttributeSet* ConvertToAtkAttributeSet(AccAttributes* aAttributes) { if (!aAttributes) { return nullptr; } AtkAttributeSet* objAttributeSet = nullptr; for (auto iter : *aAttributes) { nsAutoString name; iter.NameAsString(name); if (name.Equals(u"placeholder")) { name.AssignLiteral(u"placeholder-text"); } nsAutoString value; iter.ValueAsString(value); AtkAttribute* objAttr = (AtkAttribute*)g_malloc(sizeof(AtkAttribute)); objAttr->name = g_strdup(NS_ConvertUTF16toUTF8(name).get()); objAttr->value = g_strdup(NS_ConvertUTF16toUTF8(value).get()); objAttributeSet = g_slist_prepend(objAttributeSet, objAttr); } // libspi will free it return objAttributeSet; } AtkAttributeSet* getAttributesCB(AtkObject* aAtkObj) { Accessible* acc = GetInternalObj(aAtkObj); if (!acc) { return nullptr; } RefPtr attributes = acc->Attributes(); return ConvertToAtkAttributeSet(attributes); } const gchar* GetLocaleCB(AtkObject* aAtkObj) { Accessible* acc = GetInternalObj(aAtkObj); if (!acc) { return nullptr; } nsAutoString locale; acc->Language(locale); return AccessibleWrap::ReturnString(locale); } AtkObject* getParentCB(AtkObject* aAtkObj) { if (aAtkObj->accessible_parent) return aAtkObj->accessible_parent; Accessible* acc = GetInternalObj(aAtkObj); if (!acc) { return nullptr; } Accessible* parent = acc->Parent(); AtkObject* atkParent = parent ? GetWrapperFor(parent) : nullptr; if (atkParent) atk_object_set_parent(aAtkObj, atkParent); return aAtkObj->accessible_parent; } gint getChildCountCB(AtkObject* aAtkObj) { Accessible* acc = GetInternalObj(aAtkObj); if (!acc || nsAccUtils::MustPrune(acc)) { return 0; } return static_cast(acc->EmbeddedChildCount()); } AtkObject* refChildCB(AtkObject* aAtkObj, gint aChildIndex) { // aChildIndex should not be less than zero if (aChildIndex < 0) { return nullptr; } Accessible* acc = GetInternalObj(aAtkObj); if (!acc || nsAccUtils::MustPrune(acc)) { return nullptr; } Accessible* accChild = acc->EmbeddedChildAt(aChildIndex); if (!accChild) { return nullptr; } AtkObject* childAtkObj = GetWrapperFor(accChild); NS_ASSERTION(childAtkObj, "Fail to get AtkObj"); if (!childAtkObj) { return nullptr; } g_object_ref(childAtkObj); if (aAtkObj != childAtkObj->accessible_parent) { atk_object_set_parent(childAtkObj, aAtkObj); } return childAtkObj; } gint getIndexInParentCB(AtkObject* aAtkObj) { // We don't use LocalAccessible::IndexInParent() because we don't include text // leaf nodes as children in ATK. Accessible* acc = GetInternalObj(aAtkObj); if (!acc) { return -1; } if (acc->IsDoc()) { return 0; } Accessible* parent = acc->Parent(); if (!parent) { return -1; } return parent->IndexOfEmbeddedChild(acc); } static void TranslateStates(uint64_t aState, roles::Role aRole, AtkStateSet* aStateSet) { // atk doesn't have a read only state so read only things shouldn't be // editable. However, we don't do this for list items because Gecko always // exposes those as read only. if ((aState & states::READONLY) && aRole != roles::LISTITEM) { aState &= ~states::EDITABLE; } // Convert every state to an entry in AtkStateMap uint64_t bitMask = 1; for (auto stateIndex = 0U; stateIndex < gAtkStateMapLen; stateIndex++) { if (gAtkStateMap[stateIndex] .atkState) { // There's potentially an ATK state for this bool isStateOn = (aState & bitMask) != 0; if (gAtkStateMap[stateIndex].stateMapEntryType == kMapOpposite) { isStateOn = !isStateOn; } if (isStateOn) { atk_state_set_add_state(aStateSet, gAtkStateMap[stateIndex].atkState); } } bitMask <<= 1; } } AtkStateSet* refStateSetCB(AtkObject* aAtkObj) { AtkStateSet* state_set = nullptr; state_set = ATK_OBJECT_CLASS(parent_class)->ref_state_set(aAtkObj); if (Accessible* acc = GetInternalObj(aAtkObj)) { TranslateStates(acc->State(), acc->Role(), state_set); } else { TranslateStates(states::DEFUNCT, roles::NOTHING, state_set); } return state_set; } static void UpdateAtkRelation(RelationType aType, Accessible* aAcc, AtkRelationType aAtkType, AtkRelationSet* aAtkSet) { if (aAtkType == ATK_RELATION_NULL) return; AtkRelation* atkRelation = atk_relation_set_get_relation_by_type(aAtkSet, aAtkType); if (atkRelation) atk_relation_set_remove(aAtkSet, atkRelation); Relation rel(aAcc->RelationByType(aType)); nsTArray targets; Accessible* tempAcc = nullptr; while ((tempAcc = rel.Next())) { targets.AppendElement(GetWrapperFor(tempAcc)); } if (targets.Length()) { atkRelation = atk_relation_new(targets.Elements(), targets.Length(), aAtkType); atk_relation_set_add(aAtkSet, atkRelation); g_object_unref(atkRelation); } } AtkRelationSet* refRelationSetCB(AtkObject* aAtkObj) { AtkRelationSet* relation_set = ATK_OBJECT_CLASS(parent_class)->ref_relation_set(aAtkObj); Accessible* acc = GetInternalObj(aAtkObj); if (!acc) { return relation_set; } #define RELATIONTYPE(geckoType, geckoTypeName, atkType, msaaType, ia2Type) \ UpdateAtkRelation(RelationType::geckoType, acc, atkType, relation_set); #include "RelationTypeMap.h" #undef RELATIONTYPE return relation_set; } // Check if aAtkObj is a valid MaiAtkObject, and return the AccessibleWrap // for it. AccessibleWrap* GetAccessibleWrap(AtkObject* aAtkObj) { NS_ENSURE_TRUE(IS_MAI_OBJECT(aAtkObj), nullptr); // If we're working with an ATK object, we need to convert the Accessible // back to an AccessibleWrap: Accessible* storedAcc = MAI_ATK_OBJECT(aAtkObj)->acc; if (!storedAcc) { return nullptr; } auto* accWrap = static_cast(storedAcc->AsLocal()); // Check if the accessible was deconstructed. if (!accWrap) return nullptr; NS_ENSURE_TRUE(accWrap->GetAtkObject() == aAtkObj, nullptr); AccessibleWrap* appAccWrap = ApplicationAcc(); if (appAccWrap != accWrap && !accWrap->IsValidObject()) { return nullptr; } return accWrap; } RemoteAccessible* GetProxy(AtkObject* aObj) { Accessible* acc = GetInternalObj(aObj); if (!acc) { return nullptr; } return acc->AsRemote(); } Accessible* GetInternalObj(AtkObject* aObj) { if (!aObj || !IS_MAI_OBJECT(aObj)) return nullptr; return MAI_ATK_OBJECT(aObj)->acc; } AtkObject* GetWrapperFor(Accessible* aAcc) { if (!aAcc) { return nullptr; } if (aAcc->IsRemote()) { return reinterpret_cast(aAcc->AsRemote()->GetWrapper()); } return AccessibleWrap::GetAtkObject(aAcc->AsLocal()); } static uint16_t GetInterfacesForProxy(RemoteAccessible* aProxy) { uint16_t interfaces = 1 << MAI_INTERFACE_COMPONENT; if (aProxy->IsHyperText()) { interfaces |= (1 << MAI_INTERFACE_HYPERTEXT) | (1 << MAI_INTERFACE_TEXT) | (1 << MAI_INTERFACE_EDITABLE_TEXT); } if (aProxy->IsLink()) { interfaces |= 1 << MAI_INTERFACE_HYPERLINK_IMPL; } if (aProxy->HasNumericValue()) { interfaces |= 1 << MAI_INTERFACE_VALUE; } if (aProxy->IsTable()) { interfaces |= 1 << MAI_INTERFACE_TABLE; } if (aProxy->IsTableCell()) { interfaces |= 1 << MAI_INTERFACE_TABLE_CELL; } if (aProxy->IsImage()) { interfaces |= 1 << MAI_INTERFACE_IMAGE; } if (aProxy->IsDoc()) { interfaces |= 1 << MAI_INTERFACE_DOCUMENT; } if (aProxy->IsSelect()) { interfaces |= 1 << MAI_INTERFACE_SELECTION; } if (aProxy->IsActionable()) { interfaces |= 1 << MAI_INTERFACE_ACTION; } return interfaces; } void a11y::ProxyCreated(RemoteAccessible* aProxy) { MOZ_ASSERT(aProxy->RemoteParent() || aProxy->IsDoc(), "Need parent to check for HyperLink interface"); GType type = GetMaiAtkType(GetInterfacesForProxy(aProxy)); NS_ASSERTION(type, "why don't we have a type!"); AtkObject* obj = reinterpret_cast(g_object_new(type, nullptr)); if (!obj) return; atk_object_initialize(obj, static_cast(aProxy)); obj->role = ATK_ROLE_INVALID; obj->layer = ATK_LAYER_INVALID; aProxy->SetWrapper(reinterpret_cast(obj)); } void a11y::ProxyDestroyed(RemoteAccessible* aProxy) { auto obj = reinterpret_cast(aProxy->GetWrapper()); if (!obj) { return; } obj->Shutdown(); g_object_unref(obj); aProxy->SetWrapper(0); } void a11y::PlatformEvent(Accessible* aTarget, uint32_t aEventType) { AtkObject* wrapper = GetWrapperFor(aTarget); switch (aEventType) { case nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE: if (aTarget->IsDoc()) { g_signal_emit_by_name(wrapper, "load_complete"); } // XXX - Handle native dialog accessibles. if (!aTarget->IsRoot() && aTarget->HasARIARole() && aTarget->Role() == roles::DIALOG) { guint id = g_signal_lookup("activate", MAI_TYPE_ATK_OBJECT); g_signal_emit(wrapper, id, 0); } break; case nsIAccessibleEvent::EVENT_DOCUMENT_RELOAD: if (aTarget->IsDoc()) { g_signal_emit_by_name(wrapper, "reload"); } break; case nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_STOPPED: if (aTarget->IsDoc()) { g_signal_emit_by_name(wrapper, "load_stopped"); } break; case nsIAccessibleEvent::EVENT_MENUPOPUP_START: atk_focus_tracker_notify(wrapper); // fire extra focus event atk_object_notify_state_change(wrapper, ATK_STATE_VISIBLE, true); atk_object_notify_state_change(wrapper, ATK_STATE_SHOWING, true); break; case nsIAccessibleEvent::EVENT_MENUPOPUP_END: atk_object_notify_state_change(wrapper, ATK_STATE_VISIBLE, false); atk_object_notify_state_change(wrapper, ATK_STATE_SHOWING, false); break; case nsIAccessibleEvent::EVENT_ALERT: // A hack using state change showing events as alert events. atk_object_notify_state_change(wrapper, ATK_STATE_SHOWING, true); break; case nsIAccessibleEvent::EVENT_VALUE_CHANGE: case nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE: if (aTarget->HasNumericValue()) { // Make sure this is a numeric value. Don't fire for string value // changes (e.g. text editing) ATK values are always numeric. g_object_notify((GObject*)wrapper, "accessible-value"); } break; case nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED: g_signal_emit_by_name(wrapper, "text_selection_changed"); break; case nsIAccessibleEvent::EVENT_SELECTION_WITHIN: g_signal_emit_by_name(wrapper, "selection_changed"); break; case nsIAccessibleEvent::EVENT_TEXT_ATTRIBUTE_CHANGED: g_signal_emit_by_name(wrapper, "text-attributes-changed"); break; case nsIAccessibleEvent::EVENT_NAME_CHANGE: { nsAutoString newName; aTarget->Name(newName); MaybeFireNameChange(wrapper, newName); break; } case nsIAccessibleEvent::EVENT_WINDOW_ACTIVATE: { guint id = g_signal_lookup("activate", MAI_TYPE_ATK_OBJECT); g_signal_emit(wrapper, id, 0); // Always fire a current focus event after activation. FocusMgr()->ForceFocusEvent(); break; } case nsIAccessibleEvent::EVENT_WINDOW_DEACTIVATE: { guint id = g_signal_lookup("deactivate", MAI_TYPE_ATK_OBJECT); g_signal_emit(wrapper, id, 0); break; } case nsIAccessibleEvent::EVENT_WINDOW_MAXIMIZE: { guint id = g_signal_lookup("maximize", MAI_TYPE_ATK_OBJECT); g_signal_emit(wrapper, id, 0); break; } case nsIAccessibleEvent::EVENT_WINDOW_MINIMIZE: { guint id = g_signal_lookup("minimize", MAI_TYPE_ATK_OBJECT); g_signal_emit(wrapper, id, 0); break; } case nsIAccessibleEvent::EVENT_WINDOW_RESTORE: { guint id = g_signal_lookup("restore", MAI_TYPE_ATK_OBJECT); g_signal_emit(wrapper, id, 0); break; } } } void a11y::PlatformStateChangeEvent(Accessible* aTarget, uint64_t aState, bool aEnabled) { MaiAtkObject* atkObj = MAI_ATK_OBJECT(GetWrapperFor(aTarget)); atkObj->FireStateChangeEvent(aState, aEnabled); } void a11y::PlatformFocusEvent(Accessible* aTarget, const LayoutDeviceIntRect& aCaretRect) { AtkObject* wrapper = GetWrapperFor(aTarget); // XXX Do we really need this check? If so, do we need a similar check for // RemoteAccessible? if (LocalAccessible* localTarget = aTarget->AsLocal()) { a11y::RootAccessible* rootAcc = localTarget->RootAccessible(); if (!rootAcc || !rootAcc->IsActivated()) { return; } } atk_focus_tracker_notify(wrapper); atk_object_notify_state_change(wrapper, ATK_STATE_FOCUSED, true); } void a11y::PlatformCaretMoveEvent(Accessible* aTarget, int32_t aOffset, bool aIsSelectionCollapsed, int32_t aGranularity, const LayoutDeviceIntRect& aCaretRect, bool aFromUser) { AtkObject* wrapper = GetWrapperFor(aTarget); g_signal_emit_by_name(wrapper, "text_caret_moved", aOffset); } void MaiAtkObject::FireStateChangeEvent(uint64_t aState, bool aEnabled) { auto state = aState; int32_t stateIndex = -1; while (state > 0) { ++stateIndex; state >>= 1; } MOZ_ASSERT( stateIndex >= 0 && stateIndex < static_cast(gAtkStateMapLen), "No ATK state for internal state was found"); if (stateIndex < 0 || stateIndex >= static_cast(gAtkStateMapLen)) { return; } if (gAtkStateMap[stateIndex].atkState != kNone) { MOZ_ASSERT(gAtkStateMap[stateIndex].stateMapEntryType != kNoStateChange, "State changes should not fired for this state"); if (gAtkStateMap[stateIndex].stateMapEntryType == kMapOpposite) { aEnabled = !aEnabled; } // Fire state change for first state if there is one to map atk_object_notify_state_change(&parent, gAtkStateMap[stateIndex].atkState, aEnabled); } } void a11y::PlatformTextChangeEvent(Accessible* aTarget, const nsAString& aStr, int32_t aStart, uint32_t aLen, bool aIsInsert, bool aFromUser) { MaiAtkObject* atkObj = MAI_ATK_OBJECT(GetWrapperFor(aTarget)); atkObj->FireTextChangeEvent(aStr, aStart, aLen, aIsInsert, aFromUser); } #define OLD_TEXT_INSERTED "text_changed::insert" #define OLD_TEXT_REMOVED "text_changed::delete" static const char* oldTextChangeStrings[2][2] = { {OLD_TEXT_REMOVED NON_USER_EVENT, OLD_TEXT_INSERTED NON_USER_EVENT}, {OLD_TEXT_REMOVED, OLD_TEXT_INSERTED}}; #define TEXT_INSERTED "text-insert" #define TEXT_REMOVED "text-remove" #define NON_USER_DETAIL "::system" static const char* textChangedStrings[2][2] = { {TEXT_REMOVED NON_USER_DETAIL, TEXT_INSERTED NON_USER_DETAIL}, {TEXT_REMOVED, TEXT_INSERTED}}; void MaiAtkObject::FireTextChangeEvent(const nsAString& aStr, int32_t aStart, uint32_t aLen, bool aIsInsert, bool aFromUser) { if (gAvailableAtkSignals == eUnknown) { gAvailableAtkSignals = g_signal_lookup("text-insert", G_OBJECT_TYPE(this)) ? eHaveNewAtkTextSignals : eNoNewAtkSignals; } if (gAvailableAtkSignals == eNoNewAtkSignals) { // XXX remove this code and the gHaveNewTextSignals check when we can // stop supporting old atk since it doesn't really work anyway // see bug 619002 const char* signal_name = oldTextChangeStrings[aFromUser][aIsInsert]; g_signal_emit_by_name(this, signal_name, aStart, aLen); } else { const char* signal_name = textChangedStrings[aFromUser][aIsInsert]; g_signal_emit_by_name(this, signal_name, aStart, aLen, NS_ConvertUTF16toUTF8(aStr).get()); } } void a11y::PlatformShowHideEvent(Accessible* aTarget, Accessible* aParent, bool aInsert, bool aFromUser) { AtkObject* atkObj = GetWrapperFor(aTarget); if (!aInsert) { // XXX - Handle native dialog accessibles. if (!aTarget->IsRoot() && aTarget->HasARIARole() && aTarget->Role() == roles::DIALOG) { guint id = g_signal_lookup("deactivate", MAI_TYPE_ATK_OBJECT); g_signal_emit(atkObj, id, 0); } } MaiAtkObject* obj = MAI_ATK_OBJECT(atkObj); obj->FireAtkShowHideEvent(GetWrapperFor(aParent), aInsert, aFromUser); } #define ADD_EVENT "children_changed::add" #define HIDE_EVENT "children_changed::remove" static const char* kMutationStrings[2][2] = { {HIDE_EVENT NON_USER_EVENT, ADD_EVENT NON_USER_EVENT}, {HIDE_EVENT, ADD_EVENT}, }; void MaiAtkObject::FireAtkShowHideEvent(AtkObject* aParent, bool aIsAdded, bool aFromUser) { if (!aParent) { // XXX ATK needs a parent for these events. However, we might have already // unbound from the parent by the time we fire a hide event. Ideally, we // need to find a way to keep the parent around, but ATK clients don't seem // to care about these missing events. MOZ_ASSERT(!aIsAdded); return; } int32_t indexInParent = getIndexInParentCB(&this->parent); const char* signal_name = kMutationStrings[aFromUser][aIsAdded]; g_signal_emit_by_name(aParent, signal_name, indexInParent, this, nullptr); } void a11y::PlatformSelectionEvent(Accessible*, Accessible* aWidget, uint32_t) { MaiAtkObject* obj = MAI_ATK_OBJECT(GetWrapperFor(aWidget)); g_signal_emit_by_name(obj, "selection_changed"); } // static void AccessibleWrap::GetKeyBinding(Accessible* aAccessible, nsAString& aResult) { // Return all key bindings including access key and keyboard shortcut. // Get access key. nsAutoString keyBindingsStr; KeyBinding keyBinding = aAccessible->AccessKey(); if (!keyBinding.IsEmpty()) { keyBinding.AppendToString(keyBindingsStr, KeyBinding::eAtkFormat); Accessible* parent = aAccessible->Parent(); roles::Role role = parent ? parent->Role() : roles::NOTHING; if (role == roles::PARENT_MENUITEM || role == roles::MENUITEM || role == roles::RADIO_MENU_ITEM || role == roles::CHECK_MENU_ITEM) { // It is submenu, expose keyboard shortcuts from menu hierarchy like // "s;f:s" nsAutoString keysInHierarchyStr = keyBindingsStr; do { KeyBinding parentKeyBinding = parent->AccessKey(); if (!parentKeyBinding.IsEmpty()) { nsAutoString str; parentKeyBinding.ToString(str, KeyBinding::eAtkFormat); str.Append(':'); keysInHierarchyStr.Insert(str, 0); } } while ((parent = parent->Parent()) && parent->Role() != roles::MENUBAR); keyBindingsStr.Append(';'); keyBindingsStr.Append(keysInHierarchyStr); } } else { // No access key, add ';' to point this. keyBindingsStr.Append(';'); } // Get keyboard shortcut. keyBindingsStr.Append(';'); if (LocalAccessible* localAcc = aAccessible->AsLocal()) { keyBinding = localAcc->KeyboardShortcut(); if (!keyBinding.IsEmpty()) { keyBinding.AppendToString(keyBindingsStr, KeyBinding::eAtkFormat); } } aResult = keyBindingsStr; } // static Accessible* AccessibleWrap::GetColumnHeader(TableAccessible* aAccessible, int32_t aColIdx) { if (!aAccessible) { return nullptr; } Accessible* cell = aAccessible->CellAt(0, aColIdx); if (!cell) { return nullptr; } // If the cell at the first row is column header then assume it is column // header for all rows, if (cell->Role() == roles::COLUMNHEADER) { return cell; } // otherwise get column header for the data cell at the first row. TableCellAccessible* tableCell = cell->AsTableCell(); if (!tableCell) { return nullptr; } AutoTArray headerCells; tableCell->ColHeaderCells(&headerCells); if (headerCells.IsEmpty()) { return nullptr; } return headerCells[0]; } // static Accessible* AccessibleWrap::GetRowHeader(TableAccessible* aAccessible, int32_t aRowIdx) { if (!aAccessible) { return nullptr; } Accessible* cell = aAccessible->CellAt(aRowIdx, 0); if (!cell) { return nullptr; } // If the cell at the first column is row header then assume it is row // header for all columns, if (cell->Role() == roles::ROWHEADER) { return cell; } // otherwise get row header for the data cell at the first column. TableCellAccessible* tableCell = cell->AsTableCell(); if (!tableCell) { return nullptr; } AutoTArray headerCells; tableCell->RowHeaderCells(&headerCells); if (headerCells.IsEmpty()) { return nullptr; } return headerCells[0]; }