/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ /* * This file is part of the LibreOffice project. * * 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/. */ /* C++ wrapper for libatspi, so to make it less obnoxious to use */ /** * Adding a new wrapper * * To wrap a new Atspi type (let's say, AtspiCollection), you need to: * * 1. Add DEFINE_GOBJECT_CAST(AtspiCollection, ATSPI_TYPE_COLLECTION) near the similar * ones. This creates Atspi::cast(p) so that such a cast based on the * C++ type is checked using the GType type system. * 2. Add a declaration for the new wrapper class above Atspi::Accessible * (class Collection;) so it can be used in step 3. * 3. Add Atspi::Accessible::queryCollection() method. Its definition has to be in the * source file as it requires a complete type for the wrapper class. The body just calls * queryInterface(atspi_accessible_get_collection_iface); and returns * its value. * 4. Add the definition of the new wrapper class: * class Collection : public Accessible { ... } * Use the existing wrappers as inspiration, but basically: * - Define the constructor that only chains up to the parent * - Define each wrapper method, which generally only have to call one of the invoke() * helpers to wrap the C calls. There are a few, depending on some details of the C call: * - @c GObjectWrapperBase::invoke(): this is the most basic one, that just calls the C method * on @c GObjectWrapperBase::get() with the given arguments. Use this for calls not * throwing an exception and either returning a plain value, or something not handled by * one of the others below. * - @c AtspiWrapperBase::invokeThrow(): like @c GObjectWrapperBase::invoke(), but for C calls * that take a @c GError argument for throwing exceptions. @c invokeThrow() will * transform any C exception into a a C++ exception (@c css::uno::RuntimeException) * - @c AtspiWrapperBase::strInvoke(): like @c AtspiWrapperBase::invokeThrow(), but manages a * C string (@c char*) return value as an @c std::string. Use this for C calls returning * a C string. * - @c AtspiWrapperBase::garrayInvoke(): like @c AtspiWrapperBase::invokeThrow(), but manages * a @c GArray return value as an @c std::vector. Use this for C calls returning a * @p GArray. * - @c AtspiWrapperBase::hashMapInvoke(): like @c AtspiWrapperBase::invokeThrow(), but manages * a @c GHashTable return value as an @c std::unordered_map. Use this for C calls * returning a @p GHashTable. * - @c AtspiWrapperBase::strHashMapInvoke(): identical to @c AtspiWrapperBase::hashMapInvoke() * using C strings for keys and values. * . * If none of those match the exact return type of the C call to wrap, use * @c AtspiWrapperBase::invokeThrow() or even @c GObjectWrapperBase::invoke(), and manually * manage the result value. You can use Atspi::gmem functions to help. Basically the idea is * that you always return a self-managing object to make memory management easy (whereas it's * obnoxiously hard with plain C Atspi API). */ #pragma once #include #include #include #include #include #include #include #include namespace Atspi { /** @brief Helpers for managing GLib memory in a more C++-style */ namespace gmem { /** @brief Wraps a pointer to free with @c g_free() in a @c std::unique_ptr */ template static inline auto unique_gmem(T* ptr) { return std::unique_ptr(ptr, &g_free); } /** @brief Wraps a @c GArray to free with @c g_array_unref() in a @c std::unique_ptr */ static inline auto unique_garray(GArray* p) { return std::unique_ptr(p, &g_array_unref); } /** @brief Wraps a @c GHashTable to free with @c g_hash_table_unref() in a @c std::unique_ptr */ static inline auto unique_ghashtable(GHashTable* p) { return std::unique_ptr(p, &g_hash_table_unref); } } // --- GObject cast wrappers based on type: usage is cast(pCInstance) #define DEFINE_GOBJECT_CAST(CType, GType) \ template , int> = 1> \ P cast(T* pInstance) \ { \ return G_TYPE_CHECK_INSTANCE_CAST(pInstance, GType, std::remove_pointer_t

); \ } DEFINE_GOBJECT_CAST(AtspiStateSet, ATSPI_TYPE_STATE_SET) DEFINE_GOBJECT_CAST(AtspiRelation, ATSPI_TYPE_RELATION) DEFINE_GOBJECT_CAST(AtspiAccessible, ATSPI_TYPE_ACCESSIBLE) DEFINE_GOBJECT_CAST(AtspiComponent, ATSPI_TYPE_COMPONENT) DEFINE_GOBJECT_CAST(AtspiText, ATSPI_TYPE_TEXT) #undef DEFINE_GOBJECT_CAST // --- end GObject cast wrappers class GLibEnumBase { protected: /** * @brief Retrieves the string representation of an enumeration value * @param gt The @c GType for the enumeration * @param value The enumeration value for which to get the name for * @param fallback Fallback value in case @p values falls outside the enumeration * @returns A string representing @p value */ static std::string glibEnumValueName(GType gt, gint value, std::string_view fallback = "unknown") { auto klass = static_cast(g_type_class_ref(gt)); auto enum_value = g_enum_get_value(klass, value); std::string ret(enum_value ? enum_value->value_name : fallback); g_type_class_unref(klass); return ret; } }; class Role : private GLibEnumBase { public: static std::string getName(AtspiRole role) { return glibEnumValueName(atspi_role_get_type(), role); } }; class State : private GLibEnumBase { public: static std::string getName(AtspiStateType state) { return glibEnumValueName(atspi_state_type_get_type(), state); } }; class TextGranularity : private GLibEnumBase { public: static std::string getName(AtspiTextGranularity granularity) { return glibEnumValueName(atspi_text_granularity_get_type(), granularity); } }; class TextBoundaryType : private GLibEnumBase { public: static std::string getName(AtspiTextBoundaryType boundaryType) { return glibEnumValueName(atspi_text_boundary_type_get_type(), boundaryType); } }; /** * @brief Base class for GObject wrappers * * This leverages std::shared_ptr as a cheap way of wrapping a raw pointer, and its deleter as a * mean of using g_object_unref() to cleanup. This is sub-optimal as it maintains a separate * refcount to the GObject one, but it's easy. */ template class GObjectWrapperBase : public std::shared_ptr { public: /* this is the boundary of C++ type safety, so we can have inheritance working * properly with the C types. This should still be safe as it uses cast() which should be * defined for each using type with DEFINE_GOBJECT_CAST(), which uses GType validation */ template P get() const { return cast

(std::shared_ptr::get()); } protected: /** * @brief Calls the C function @p f on the C object wrapped by @p this * @param f The C function to call * @param args Additional arguments to @p f * @returns The return value from @p f * * Calls the C function @p f similar to f(get(), args...). Care is taken of * transforming @c get() to the type actually expected as the first argument of @p f, using * @c get(), which performs a runtime verification of the conversion. * * @note The type verification on whether @p f actually takes what get() returns is performed * at runtime, so there will be no compilation error or warning if trying to use an * incompatible C function. A check will however be performed at runtime, at least * helping diagnose a possible invalid conversion. */ template inline auto invoke(F f, Ts... args) const { using FT = std::remove_pointer_t; const auto p = get::arg1_type>(); return f(p, args...); } private: static void deleter(T* p) { if (p) g_object_unref(p); } public: /** * @param pObj The raw GObject to wrap * @param takeRef Whether to take ownership of the object or not. If set to @c false, it will * call @c g_object_ref(pAcc) to acquire a new reference to the GObject. */ GObjectWrapperBase(T* pObj = nullptr, bool takeRef = true) : std::shared_ptr(pObj, deleter) { if (pObj && !takeRef) g_object_ref(pObj); } }; /** @brief AtspiStateSet C++ wrapper */ class StateSet : public GObjectWrapperBase { public: using GObjectWrapperBase::GObjectWrapperBase; void add(const AtspiStateType t) { invoke(atspi_state_set_add, t); } StateSet compare(const StateSet& other) const { return StateSet(invoke(atspi_state_set_compare, other.get())); } bool contains(const AtspiStateType t) const { return invoke(atspi_state_set_contains, t); } bool operator==(const StateSet& other) const { return invoke(atspi_state_set_equals, other.get()); } std::vector getStates() const { auto garray = gmem::unique_garray(invoke(atspi_state_set_get_states)); std::vector states; for (auto i = decltype(garray->len){ 0 }; i < garray->len; i++) { states.push_back(g_array_index(garray, decltype(states)::value_type, i)); } return states; } bool empty() const { return invoke(atspi_state_set_is_empty); } void remove(AtspiStateType t) { invoke(atspi_state_set_remove, t); } void setByName(const std::string_view name, bool enable) { invoke(atspi_state_set_set_by_name, name.data(), enable); } }; class Accessible; /** @brief AtspiRelation C++ wrapper */ class Relation : public GObjectWrapperBase { public: using GObjectWrapperBase::GObjectWrapperBase; AtspiRelationType getRelationType() const { return invoke(atspi_relation_get_relation_type); } int getNTargets() const { return invoke(atspi_relation_get_n_targets); } Accessible getTarget(int i) const; }; /* intermediate base just for splitting out the *invoke* helpers implementations, so the actual * user-targeted class can hold only the actual API */ template class AtspiWrapperBase : public GObjectWrapperBase { protected: using GObjectWrapperBase::invoke; /** * @brief Calls the throwing C function @p f on the C object wrapped by @p this * @param f The C function to call * @param args Additional arguments to @p f * @returns The raw return value from @p f * @throws css::uno::RuntimeException Exception @c GError are translated to * * This wrapper calls @p f with parameters @p args and an additional @c GError parameter to * catch C exception, transforming them into C++ exceptions of type * @c css::uno::RuntimeException. * * @see invoke() */ template inline auto invokeThrow(F f, Ts... args) const { GError* err = nullptr; auto ret = invoke(f, args..., &err); if (err) { throw css::uno::RuntimeException(OUString::fromUtf8(err->message)); } return ret; } /** * @brief Calls the throwing C function @p f on the C object wrapped by @p this and returns a string * @param f The C function to call * @param args Additional arguments to @p f * @tparam E the type of exception to throw if @p f returns @c null, defaults to * @c css::uno::RuntimeException * @returns A string holding the return value from @p f * @throws css::uno::RuntimeException See invokeThrow() * @throws E Exception to use if @p f returns null with no other error * * Just like @c invokeThrow(), but wraps the return value in an @c std::string and manages the * lifetime of the C function return value. * * As @c std::string cannot represent a @c null value, if @p f returned such a value without * throwing an exception, this method will throw an exception of type @p E * * @see invokeThrow() */ template inline std::string strInvoke(F f, Ts... args) const { auto r = invokeThrow(f, args...); /* if the API returned NULL without throwing, use the specified exception because a nullptr * std::string is not valid, and std::logic_error isn't gonna be very useful to the caller */ if (!r) throw E(); return gmem::unique_gmem(r).get(); } /** * @brief Calls the throwing C function @p f on the C object wrapped by @p this and returns a vector * @param f The C function to call * @param args Additional arguments to @p f * @tparam Vi The type of the members of the @c GArray @p f returns * @tparam Vo The type of the members of the returned vector * @returns A vector holding the return value from @p f * @throws css::uno::RuntimeException See invokeThrow() * * Just like @c invokeThrow(), but wraps the return in an @c std::vector and manages the * lifetime of the C function return value. * * @p Vi has to be implicitly convertible to @p Vo. A typical usage could be * garrayInvoke(...), which would transform a @c GArray * of @c AtspiAccessible* to an @c std::vector of @c Atspi::Accessible. * * @warning You have to get @p Vi right, there is no way to validate this type is correct or * not, so you won't get a compilation error nor even a warning if you give the wrong * type here. * * @see invokeThrow() */ template inline std::vector garrayInvoke(F f, Ts... args) const { auto garray = gmem::unique_garray(invokeThrow(f, args...)); std::vector vec; for (auto i = decltype(garray->len){ 0 }; i < garray->len; i++) vec.push_back(g_array_index(garray, Vi, i)); return vec; } /** * @brief Wraps an AT-SPI call returning a GHashTable * @tparam Ki Type of the keys in the wrapped hash table * @tparam Vi Type of the values in the wrapped hash table * @tparam Ko Type of the keys in the wrapper map (this must be convertible from Ki) * @tparam Vo Type of the values in the wrapper map (this must be convertible from Kv) * @param f The function to call * @param args Arguments to pass to @p f * @returns A @c std::unordered_map holding the data returned by @p f. * * @see invokeThrow() * @see strHashMapInvoke() */ template inline std::unordered_map hashMapInvoke(F f, Ts... args) const { auto ghash = gmem::unique_ghashtable(invokeThrow(f, args...)); std::unordered_map map; GHashTableIter iter; g_hash_table_iter_init(&iter, ghash.get()); gpointer key, value; while (g_hash_table_iter_next(&iter, &key, &value)) { map.emplace(static_cast(key), static_cast(value)); } return map; } /** * @brief Just like @c hashMapInvoke() but already specialized for strings * @param f The C function to call * @param args Arguments to @p f * @returns A @c std::unordered_map holding the data returned by @p f * * This is exactly the same as * hashMapInvoke(f, args...) */ template inline auto strHashMapInvoke(F f, Ts... args) const { return hashMapInvoke(f, args...); } public: using GObjectWrapperBase::GObjectWrapperBase; }; class Component; class Text; /** * @brief AtspiAccessible C++ wrapper * * This is a wrapper for the AtspiAccessible GObject class to make it a bit nicer to use in C++, * including a proper class with methods, regular exceptions, easy memory management, and an * iterator to enumerate children. * * As this class actually inherits from std::shared_ptr, you can easily use @c get() to retrieve * the wrapped pointer in case you need to, e.g. if some specific API is missing. However, take * care of memory management on that object not to have it destroyed early (e.g. don't let anyone * call @c g_object_unref() on it if they didn't call g_object_ref() first). * * To use it, just wrap an initial AtspiAccessible using the class constructor. * @code * Atspi::Accessible desktop(atspi_get_desktop(0)); * for (auto&& app: desktop) { * std::cout << app->getName() << std::endl; * } * @endcode * * For details on the specific methods, see the C Atspi documentation. */ class Accessible : public AtspiWrapperBase { public: using AtspiWrapperBase::AtspiWrapperBase; void setCacheMask(AtspiCache mask) const { invoke(atspi_accessible_set_cache_mask, mask); } void clearCache() const { invoke(atspi_accessible_clear_cache); } AtspiRole getRole() const { return invokeThrow(atspi_accessible_get_role); } std::string getRoleName() const { return strInvoke(atspi_accessible_get_role_name); } std::string getName() const { return strInvoke(atspi_accessible_get_name); } std::string getDescription() const { return strInvoke(atspi_accessible_get_description); } int getChildCount() const { return invokeThrow(atspi_accessible_get_child_count); } Accessible getChildAtIndex(int idx) const { return Accessible(invokeThrow(atspi_accessible_get_child_at_index, idx)); } int getIndexInParent() const { return invokeThrow(atspi_accessible_get_index_in_parent); } Accessible getParent() const { return Accessible(invokeThrow(atspi_accessible_get_parent)); } StateSet getStateSet() const { return StateSet(invoke(atspi_accessible_get_state_set)); } std::unordered_map getAttributes() const { return strHashMapInvoke(atspi_accessible_get_attributes); } std::vector getRelationSet() const { return garrayInvoke(atspi_accessible_get_relation_set); } private: template I queryInterface(F f) const { auto pIface = invoke(f); if (pIface) return I(pIface); throw css::uno::RuntimeException("Not implemented"); } public: Component queryComponent() const; Text queryText() const; // convenience extensions class iterator { public: using iterator_category = std::input_iterator_tag; using value_type = Accessible; using difference_type = std::ptrdiff_t; using reference = value_type&; using pointer = value_type*; private: const Accessible* m_pAccessible; int m_idx; public: explicit iterator(const Accessible* pAccessible, int idx = 0) : m_pAccessible(pAccessible) , m_idx(idx) { } iterator(iterator const& other) : m_pAccessible(other.m_pAccessible) , m_idx(other.m_idx) { } iterator& operator++() { m_idx++; return *this; } iterator operator++(int) { iterator other = *this; ++(*this); return other; } bool operator==(iterator other) const { return m_idx == other.m_idx && m_pAccessible == other.m_pAccessible; } bool operator!=(iterator other) const { return !(*this == other); } value_type operator*() const { assert(m_idx < m_pAccessible->getChildCount()); return m_pAccessible->getChildAtIndex(m_idx); } }; iterator begin() const { return iterator(this); } iterator end() const { if (!get()) return iterator(this); return iterator(this, std::max(0, getChildCount())); } }; // we just use ATSPI's own structure here, but pass it by value using Rect = AtspiRect; using Point = AtspiPoint; /** @brief AtspiComponent C++ wrapper */ class Component : public Accessible { public: Component(AtspiComponent* pObj = nullptr, bool takeRef = true) : Accessible(cast(pObj), takeRef) { } bool contains(int x, int y, AtspiCoordType coordType) const { return invokeThrow(atspi_component_contains, x, y, coordType); } Accessible getAccessibleAtPoint(int x, int y, AtspiCoordType coordType) const { return invokeThrow(atspi_component_get_accessible_at_point, x, y, coordType); } Rect getExtents(AtspiCoordType coordType) const { return *gmem::unique_gmem(invokeThrow(atspi_component_get_extents, coordType)); } Point getPosition(AtspiCoordType coordType) const { return *gmem::unique_gmem(invokeThrow(atspi_component_get_position, coordType)); } Point getSize() const { return *gmem::unique_gmem(invokeThrow(atspi_component_get_size)); } AtspiComponentLayer getLayer() const { return invokeThrow(atspi_component_get_layer); } short getMdiZOrder() const { return invokeThrow(atspi_component_get_mdi_z_order); } bool grabFocus() const { return invokeThrow(atspi_component_grab_focus); } double getAlpha() const { return invokeThrow(atspi_component_get_alpha); } #if HAVE_ATSPI2_SCROLL_TO bool scrollTo(AtspiScrollType scrollType) const { return invokeThrow(atspi_component_scroll_to, scrollType); } bool scrollToPoint(AtspiCoordType coordType, int x, int y) const { return invokeThrow(atspi_component_scroll_to_point, coordType, x, y); } #endif // HAVE_ATSPI2_SCROLL_TO }; /** @brief AtspiText C++ wrapper */ class Text : public Accessible { public: Text(AtspiText* pObj = nullptr, bool takeRef = true) : Accessible(cast(pObj), takeRef) { } /** Wrapper for AtspiRange * * This is not actually required, but helps make this more C++-y (by allowing TextRange to * inherit it) and more LibreOffice-y (by having camelCase names) */ struct Range { int startOffset; int endOffset; Range(int startOffset_, int endOffset_) : startOffset(startOffset_) , endOffset(endOffset_) { } Range(const AtspiRange* r) : startOffset(r->start_offset) , endOffset(r->end_offset) { } }; /** Wrapper for AtspiTextRange */ struct TextRange : Range { std::string content; TextRange(int startOffset_, int endOffset_, std::string_view content_) : Range(startOffset_, endOffset_) , content(content_) { } TextRange(const AtspiTextRange* r) : Range(r->start_offset, r->end_offset) , content(r->content) { } }; int getCharacterCount() const { return invokeThrow(atspi_text_get_character_count); } std::string getText(int startOffset, int endOffset) const { return strInvoke(atspi_text_get_text, startOffset, endOffset); } int getCaretOffset() const { return invokeThrow(atspi_text_get_caret_offset); } std::unordered_map getTextAttributes(int offset, int* startOffset, int* endOffset) const { return strHashMapInvoke(atspi_text_get_text_attributes, offset, startOffset, endOffset); } std::unordered_map getAttributeRun(int offset, bool includeDefaults, int* startOffset, int* endOffset) const { return strHashMapInvoke(atspi_text_get_attribute_run, offset, includeDefaults, startOffset, endOffset); } std::unordered_map getDefaultAttributes() const { return strHashMapInvoke(atspi_text_get_default_attributes); } std::string getTextAttributeValue(int offset, std::string_view name) const { return strInvoke( atspi_text_get_text_attribute_value, offset, const_cast(name.data())); } protected: /** Like @c invokeThrow() on C calls returning an @c AtspiTextRange */ template inline TextRange invokeTextRange(F f, Ts... args) const { struct deleter { void operator()(AtspiTextRange* ptr) { g_free(ptr->content); g_free(ptr); } }; std::unique_ptr r(invokeThrow(f, args...)); return r.get(); } public: TextRange getStringAtOffset(int offset, AtspiTextGranularity granularity) const { return invokeTextRange(atspi_text_get_string_at_offset, offset, granularity); } /* the next 3 are deprecated, but LO doesn't implement getStringAtOffset() itself so it's a lot * trickier to test for */ TextRange getTextBeforeOffset(int offset, AtspiTextBoundaryType boundary) const { return invokeTextRange(atspi_text_get_text_before_offset, offset, boundary); } TextRange getTextAtOffset(int offset, AtspiTextBoundaryType boundary) const { return invokeTextRange(atspi_text_get_text_at_offset, offset, boundary); } TextRange getTextAfterOffset(int offset, AtspiTextBoundaryType boundary) const { return invokeTextRange(atspi_text_get_text_after_offset, offset, boundary); } sal_Int32 getCharacterAtOffset(int offset) const { return invokeThrow(atspi_text_get_character_at_offset, offset); } Rect getCharacterExtents(int offset, AtspiCoordType type) const { return *gmem::unique_gmem(invokeThrow(atspi_text_get_character_extents, offset, type)); } int getOffsetAtPoint(int x, int y, AtspiCoordType type) const { return invokeThrow(atspi_text_get_offset_at_point, x, y, type); } Rect getRangeExtents(int startOffset, int endOffset, AtspiCoordType type) const { return *gmem::unique_gmem( invokeThrow(atspi_text_get_range_extents, startOffset, endOffset, type)); } // getBoundedRanges() ? int getNSelections() const { return invokeThrow(atspi_text_get_n_selections); } Range getSelection(gint selectionNum) const { return gmem::unique_gmem(invokeThrow(atspi_text_get_selection, selectionNum)).get(); } bool addSelection(int startOffset, int endOffset) const { return invokeThrow(atspi_text_add_selection, startOffset, endOffset); } bool removeSelection(int selectionNum) const { return invokeThrow(atspi_text_remove_selection, selectionNum); } bool setSelection(int selectionNum, int startOffset, int endOffset) const { return invokeThrow(atspi_text_set_selection, selectionNum, startOffset, endOffset); } #if HAVE_ATSPI2_SCROLL_TO bool scrollSubstringTo(int startOffset, int endOffset, AtspiScrollType type) const { return invokeThrow(atspi_text_scroll_substring_to, startOffset, endOffset, type); } bool scrollSubstringToPoint(int startOffset, int endOffset, AtspiCoordType coords, gint x, gint y) const { return invokeThrow(atspi_text_scroll_substring_to_point, startOffset, endOffset, coords, x, y); } #endif // HAVE_ATSPI2_SCROLL_TO }; } // CppUnit integration CPPUNIT_NS_BEGIN template <> struct assertion_traits { static bool equal(const AtspiRole a, const AtspiRole b) { return a == b; } static std::string toString(const AtspiRole role) { return Atspi::Role::getName(role); } }; CPPUNIT_NS_END /* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */