Add structural navigation. Pivot chooser and cache.
authorLukasz Stanislawski <l.stanislaws@samsung.com>
Thu, 13 Nov 2014 09:23:31 +0000 (10:23 +0100)
committerLukasz Stanislawski <l.stanislaws@samsung.com>
Fri, 28 Nov 2014 15:00:48 +0000 (16:00 +0100)
Pivot_chooser is a module selecting an object which should have
'accessible focus'.

Structural navi modules enables to walk accessible object hierarchy.
OneFinger flick gesture allows travers along sibling widgets,
TwoFilger flick travers to parent or to children widget.

Cache allows to fast access to AtsComponent data.

include/gesture_tracker.h
include/object_cache.h [new file with mode: 0644]
include/pivot_chooser.h [new file with mode: 0644]
include/position_sort.h [new file with mode: 0644]
include/structural_navi.h [new file with mode: 0644]
src/gesture_tracker.c
src/navigator.c
src/object_cache.c [new file with mode: 0644]
src/pivot_chooser.c [new file with mode: 0644]
src/position_sort.c [new file with mode: 0644]
src/structural_navi.c [new file with mode: 0644]

index b1355bb..5ae3ea9 100644 (file)
@@ -17,6 +17,10 @@ enum _Gesture {
      THREE_FINGERS_FLICK_DOWN,
      ONE_FINGER_SINGLE_TAP,
      ONE_FINGER_DOUBLE_TAP,
+     TWO_FINGERS_FLICK_UP,
+     TWO_FINGERS_FLICK_LEFT,
+     TWO_FINGERS_FLICK_RIGHT,
+     TWO_FINGERS_FLICK_DOWN,
      GESTURES_COUNT,
 };
 typedef enum _Gesture Gesture;
diff --git a/include/object_cache.h b/include/object_cache.h
new file mode 100644 (file)
index 0000000..cd97d67
--- /dev/null
@@ -0,0 +1,58 @@
+#ifndef SMART_NAVI_OBJECT_CACHE_H_H
+#define SMART_NAVI_OBJECT_CACHE_H_H
+
+#include <atspi/atspi.h>
+
+typedef struct {
+    // Boundaries of object taken from atspi_component_get_extents function.
+    // NULL if object do not implemetn AtspiComponent interface
+    AtspiRect *bounds;
+} ObjectCache;
+
+typedef void (*ObjectCacheReadyCb)(void *data);
+
+/**
+ * @brief Recursivly build ObjectCache structures for root Accessible
+ * object from and its descendants. (Children's children also, etc.)
+ *
+ * @param root starting object.
+ *
+ * @remarks This function may block main-loop for significant ammount of time.
+ *          Flushes all previously cached items.
+ */
+void object_cache_build(AtspiAccessible *root);
+
+/**
+ * @brief Recursivly build ObjectCache structures for ever Accessible
+ * object from root and its all descendants.
+ *
+ * @param root starting object.
+ * @param bulk_size Ammount of AtspiAccessible that will be cached on every
+ * ecore_main_loop entering into 'idle' state.
+ * @param cb function to be called after creating cache is done.
+ * @param user_data passed to cb
+ *
+ * @remarks Flushes all previously cached items.
+ */
+void object_cache_build_async(AtspiAccessible *root, int bulk_size, ObjectCacheReadyCb cb, void *user_data);
+
+/**
+ * @brief Gets and ObjectCache item from accessible object
+ *
+ * @param obj AtspiAccessible
+ *
+ * @remarks If obj is not in cache function will block main loop and build ObjectCache
+ * struct.
+ */
+const ObjectCache *object_cache_get(AtspiAccessible *obj);
+
+/**
+ * @brief Shoutdown cache.
+ *
+ * Function will free all gathered caches.
+ *
+ * @remarks Use it after You have used any of above object_cache_* functions.
+ */
+void object_cache_shutdown(void);
+
+#endif /* end of include guard: OBJECT_CACHE_H_H */
diff --git a/include/pivot_chooser.h b/include/pivot_chooser.h
new file mode 100644 (file)
index 0000000..164e84c
--- /dev/null
@@ -0,0 +1,15 @@
+#ifndef SMART_NAVI_PIVOT_CHOOSER_H_
+#define SMART_NAVI_PIVOT_CHOOSER_H_
+
+#include <atspi/atspi.h>
+
+/**
+ * @brief Some heuristic choosing candidate to reacieve highlight.
+ *
+ * @param win Accessibility search tree object root.
+ *
+ * @return Highlight candidate
+ */
+AtspiAccessible *pivot_chooser_pivot_get(AtspiAccessible *win);
+
+#endif /* end of include guard: PIVOT_CHOOSER_H_ */
diff --git a/include/position_sort.h b/include/position_sort.h
new file mode 100644 (file)
index 0000000..64d3e50
--- /dev/null
@@ -0,0 +1,15 @@
+#ifndef SMART_NAVI_POSITION_SORT_H_
+#define SMART_NAVI_POSITION_SORT_H_
+
+#include <atspi/atspi.h>
+#include <Eina.h>
+
+
+/**
+ * @brief Sort objects by position they appear on screen
+ *
+ * @ret list List of lists
+ */
+Eina_List *position_sort(const Eina_List *obj);
+
+#endif /* end of include guard: POSITION_SORT_H_ */
diff --git a/include/structural_navi.h b/include/structural_navi.h
new file mode 100644 (file)
index 0000000..64db592
--- /dev/null
@@ -0,0 +1,15 @@
+#ifndef SMART_NAVI_STRUCTURAL_NAVI_H
+#define SMART_NAVI_STRUCTURAL_NAVI_H_
+
+#include <atspi/atspi.h>
+
+AtspiAccessible *structural_navi_same_level_next(AtspiAccessible *current);
+AtspiAccessible *structural_navi_same_level_prev(AtspiAccessible *current);
+
+AtspiAccessible *structural_navi_level_up(AtspiAccessible *current);
+AtspiAccessible *structural_navi_level_down(AtspiAccessible *current);
+
+AtspiAccessible *structural_navi_app_chain_next(AtspiAccessible *current);
+AtspiAccessible *structural_navi_app_chain_prev(AtspiAccessible *current);
+
+#endif /* end of include guard: STRUCTURAL_NAVI_KML0ETIJ */
index 867136e..1db8d10 100644 (file)
@@ -56,6 +56,18 @@ static Gesture gesture_name_to_enum (const char *gesture_name)
  if(!strcmp("OneFingerDoubleTap", gesture_name))
      return ONE_FINGER_DOUBLE_TAP;
 
+ if(!strcmp("TwoFingersFlickLeft", gesture_name))
+    return TWO_FINGERS_FLICK_LEFT;
+
+ if(!strcmp("TwoFingersFlickRight", gesture_name))
+    return TWO_FINGERS_FLICK_RIGHT;
+
+ if(!strcmp("TwoFingersFlickUp", gesture_name))
+    return TWO_FINGERS_FLICK_UP;
+
+ if(!strcmp("TwoFingersFlickDown", gesture_name))
+     return TWO_FINGERS_FLICK_DOWN;
+
  return GESTURES_COUNT;
 }
 
index ff8a60e..dc62a23 100644 (file)
@@ -5,6 +5,9 @@
 #include "gesture_tracker.h"
 #include "window_tracker.h"
 #include "keyboard_tracker.h"
+#include "pivot_chooser.h"
+#include "structural_navi.h"
+#include "object_cache.h"
 
 #define QUICKPANEL_DOWN TRUE
 #define QUICKPANEL_UP FALSE
 static AtspiAccessible *previous_obj;
 static AtspiAccessible *current_obj;
 static AtspiAccessible *top_window;
+static AtspiScrollable *scrolled_obj;
+static Eina_Bool _window_cache_builded;
+
+static void
+_current_highlight_object_set(AtspiAccessible *obj)
+{
+   if (!obj)
+     {
+        atspi_component_clear_highlight(top_window);
+        return;
+     }
+   if (current_obj == obj)
+     {
+        DEBUG("Object already highlighted");
+        return;
+     }
+    if (obj && ATSPI_IS_COMPONENT(obj))
+      {
+         atspi_component_clear_highlight(top_window);
+         atspi_component_grab_highlight(obj, NULL);
+         current_obj = obj;
+         DEBUG("New highlighted object: %s, role: %s",
+               atspi_accessible_get_name(obj, NULL),
+               atspi_accessible_get_role_name(obj, NULL));
+      }
+    else
+       DEBUG("Unable to highlight object");
+}
 
 void test_debug(AtspiAccessible *current_widget)
 {
@@ -117,8 +148,7 @@ static void find_objects(AtspiAccessible* parent, gint x, gint y, gint radius, d
 
         int height = 0, width = 0, xpos1 = 0, ypos1 = 0, xpos2 = 0, ypos2 = 0;
 
-        object_get_wh(app, &width, &height);
-        object_get_x_y(app, &xpos1, &ypos1);
+        object_get_wh(app, &width, &height); object_get_x_y(app, &xpos1, &ypos1);
 
         gboolean is_visile = atspi_state_set_contains(state_set, state);
 
@@ -183,7 +213,7 @@ static AtspiAccessible *get_nearest_widget(AtspiAccessible* app_obj, gint x_cord
 }
 
 static void _focus_widget(Gesture_Info *info)
-{/*
+{
     AtspiAccessible *target_widget = NULL;
     AtspiComponent *target_component = NULL;
     AtspiComponent *window_component = NULL;
@@ -193,32 +223,10 @@ static void _focus_widget(Gesture_Info *info)
         return;
 
     target_widget = atspi_component_get_accessible_at_point(window_component, info->x_begin, info->y_begin, ATSPI_COORD_TYPE_WINDOW, NULL);
-    if(!target_widget)
-        return;
-
-    target_component = atspi_accessible_get_component(target_widget);
-    if(!target_component)
-        return;
+    if (target_widget) _current_highlight_object_set(target_widget);
     else
-    {
-        if (atspi_component_grab_focus(target_component, NULL) == TRUE)
-        {
-            ERROR("Focus was changed\n");
-            current_obj = target_widget;
-            border_set(current_obj);
-        }
-    }
-*/
-    AtspiAccessible *current_widget  = get_nearest_widget(top_window, info->x_begin, info->y_begin, -1);
-    if(current_widget)
-    {
-        current_obj = current_widget;
-        AtspiComponent *focus_component = atspi_accessible_get_component(current_widget);
-
-        if (atspi_component_grab_highlight(focus_component, NULL))
-            ERROR("Highlighted\n");
-
-    }
+      DEBUG("NO widget under (%d, %d) found",
+            info->x_begin, info->y_begin);
 }
 
 static AtspiAccessible *_focused_next_widget(void)
@@ -321,12 +329,20 @@ static AtspiAccessible *_focused_prev_widget(void)
 
 static void _focus_next(void)
 {
-    current_obj = _focused_next_widget();
+    AtspiAccessible *obj = structural_navi_same_level_next(current_obj);
+    if (obj)
+      _current_highlight_object_set(obj);
+    else
+      DEBUG("Next widget not found. Abort");
 }
 
 static void _focus_prev(void)
 {
-    current_obj = _focused_prev_widget();
+    AtspiAccessible *obj = structural_navi_same_level_prev(current_obj);
+    if (obj)
+      _current_highlight_object_set(obj);
+    else
+      DEBUG("Previous widget not found. Abort");
 }
 
 static void _value_inc_widget(void)
@@ -470,12 +486,12 @@ static void _activate_widget(void)
     if(!strcmp(roleName, "entry"))
     {
         focus_component = atspi_accessible_get_component(current_widget);
-               if (focus_component != NULL)
+        if (focus_component != NULL)
         {
-                       if (atspi_component_grab_focus(focus_component, NULL) == TRUE)
-                               ERROR("Entry activated\n");
-                       return;
-               }
+            if (atspi_component_grab_focus(focus_component, NULL) == TRUE)
+                ERROR("Entry activated\n");
+            return;
+        }
     }
 
     AtspiAction *action;
@@ -519,7 +535,7 @@ static void _quickpanel_change_state(gboolean quickpanel_switch)
     const char *clas = NULL;
     int ret;
 
-    ERROR(quickpanel_switch ? "QUICKPANEL STATE ON" : "QUICKPANEL STATE OFF");  
+    ERROR(quickpanel_switch ? "QUICKPANEL STATE ON" : "QUICKPANEL STATE OFF");
 
     Ecore_X_Illume_Quickpanel_State state;
 
@@ -539,6 +555,168 @@ static void _quickpanel_change_state(gboolean quickpanel_switch)
     ecore_main_loop_iterate();
 }
 
+/**
+ * @brief Gets 'deepest' Scrollable accessible containing (x,y) point
+ */
+static AtspiScrollable*
+_find_scrollable_ancestor_at_xy(int x, int y)
+{
+   AtspiAccessible *ret = NULL;
+   AtspiRect *rect;
+   GError *error = NULL;
+
+   if (!top_window || !ATSPI_IS_COMPONENT(top_window))
+     {
+        DEBUG("No active window detected or no AtspiComponent interface available");
+        return NULL;
+     }
+
+   rect = atspi_component_get_extents(ATSPI_COMPONENT(top_window), ATSPI_COORD_TYPE_SCREEN, &error);
+   if (!rect || error)
+     {
+        ERROR("Unable to fetch window screen coordinates");
+        if (error) g_error_free(error);
+        return NULL;
+     }
+
+   // Scroll must originate within window borders
+   if ((x < rect->x) || (x > rect->x + rect->width) ||
+       (y < rect->y) || (y > rect->y + rect->height))
+     {
+        DEBUG("Scroll don't start within active window borders");
+        g_free(rect);
+        return NULL;
+     }
+
+   ret = atspi_component_get_accessible_at_point(ATSPI_COMPONENT(top_window), x, y, ATSPI_COORD_TYPE_SCREEN, &error);
+   if (!ret || error)
+     {
+        ERROR("Unable to get accessible objct at (%d, %d) screen coordinates.", x, y);
+        if (error) g_error_free(error);
+        return NULL;
+     }
+
+   // find accessible object with Scrollable interface
+   while (ret && (ret != top_window))
+     {
+        DEBUG("Testing for scrollability: %s %s",
+                   atspi_accessible_get_name(ret, NULL), atspi_accessible_get_role_name(ret, NULL));
+        if (atspi_accessible_get_scrollable(ret))
+          {
+             DEBUG("Scrollable widget found at (%d, %d), name: %s, role: %s", x, y,
+                   atspi_accessible_get_name(ret, NULL), atspi_accessible_get_role_name(ret, NULL));
+             return ATSPI_SCROLLABLE(ret);
+          }
+        ret = atspi_accessible_get_parent(ret, &error);
+        if (error)
+          {
+             ERROR("Unable to fetch AT-SPI parent");
+             g_error_free(error);
+             return NULL;
+          }
+     }
+
+   return NULL;
+}
+
+static void _widget_scroll_begin(Gesture_Info *gi)
+{
+   GError *error = NULL;
+
+   if (scrolled_obj)
+     {
+        ERROR("Scrolling context active when initializing new scrolling context! This should never happen.");
+        ERROR("Force reset of current scrolling context...");
+        atspi_scrollable_scroll_after_pointer(scrolled_obj, ATSPI_SCROLL_POINTER_END, gi->x_begin, gi->y_begin, &error);
+        if (error)
+          {
+             ERROR("Failed to reset scroll context.");
+             g_error_free(error);
+             scrolled_obj = NULL;
+             error = NULL;
+          }
+     }
+
+   scrolled_obj = _find_scrollable_ancestor_at_xy(gi->x_begin, gi->y_begin);
+
+   if (!scrolled_obj)
+     {
+        DEBUG("No scrollable widget found at (%d, %d) coordinates", gi->x_begin, gi->y_begin);
+        return;
+     }
+
+   atspi_scrollable_scroll_after_pointer(scrolled_obj, ATSPI_SCROLL_POINTER_START, gi->x_begin, gi->y_begin, &error);
+   if (error)
+     {
+        ERROR("Failed to initialize scroll operation");
+        scrolled_obj = NULL;
+     }
+}
+
+static void _widget_scroll_continue(Gesture_Info *gi)
+{
+   if (!scrolled_obj)
+     {
+        DEBUG("Scrolling context not initialized!");
+        return;
+     }
+   atspi_scrollable_scroll_after_pointer(scrolled_obj, ATSPI_SCROLL_POINTER_CONTINUE, gi->x_begin, gi->y_begin, NULL);
+}
+
+static void _widget_scroll_end(Gesture_Info *gi)
+{
+   if (!scrolled_obj)
+     {
+        ERROR("Scrolling context not initialized!");
+        return;
+     }
+
+   atspi_scrollable_scroll_after_pointer(scrolled_obj, ATSPI_SCROLL_POINTER_END, gi->x_begin, gi->y_begin, NULL);
+   scrolled_obj = NULL;
+}
+
+static void _goto_children_widget(void)
+{
+   AtspiAccessible *obj;
+   if (!current_obj)
+     {
+        DEBUG("No current object is set. Aborting diving into children structure");
+        return;
+     }
+   obj = structural_navi_level_down(current_obj);
+   if (obj) _current_highlight_object_set(obj);
+}
+
+static void _escape_children_widget(void)
+{
+   AtspiAccessible *obj;
+   if (!current_obj)
+     {
+        DEBUG("No current object is set. Aborting escaping from children structure");
+        return;
+     }
+   obj = structural_navi_level_up(current_obj);
+   if (obj) _current_highlight_object_set(obj);
+}
+
+static void _widget_scroll(Gesture_Info *gi)
+{
+   switch (gi->state)
+     {
+      case 0:
+         _widget_scroll_begin(gi);
+         break;
+      case 1:
+         _widget_scroll_continue(gi);
+         break;
+      case 2:
+         _widget_scroll_end(gi);
+         break;
+      default:
+         ERROR("Unrecognized gesture state: %d", gi->state);
+     }
+}
+
 static void on_gesture_detected(void *data, Gesture_Info *info)
 {
    switch(info->type)
@@ -547,7 +725,7 @@ static void on_gesture_detected(void *data, Gesture_Info *info)
           _focus_widget(info);
           break;
       case TWO_FINGERS_HOVER:
-          ERROR("SCROLLING!");
+          _widget_scroll(info);
           break;
       case ONE_FINGER_FLICK_LEFT:
           _focus_prev();
@@ -567,11 +745,11 @@ static void on_gesture_detected(void *data, Gesture_Info *info)
       case ONE_FINGER_DOUBLE_TAP:
           _activate_widget();
           break;
-      case THREE_FINGERS_FLICK_LEFT:
-          // not implemented ??
+      case TWO_FINGERS_FLICK_LEFT:
+          _escape_children_widget();
           break;
-      case THREE_FINGERS_FLICK_RIGHT:
-          // not implemented ??
+      case TWO_FINGERS_FLICK_RIGHT:
+          _goto_children_widget();
           break;
       case THREE_FINGERS_FLICK_DOWN:
           _quickpanel_change_state(QUICKPANEL_DOWN);
@@ -580,70 +758,69 @@ static void on_gesture_detected(void *data, Gesture_Info *info)
           _quickpanel_change_state(QUICKPANEL_UP);
           break;
       default:
-          ERROR("Function not implemented for gesture type :%d", info->type);
+          DEBUG("Gesture type %d not handled in switch", info->type);
    }
 }
 
 static void _get_first_focusable_widget(AtspiAccessible* obj)
 {
-       AtspiAccessible *current_widget = NULL;
-       AtspiStateSet *state_set = NULL;
-       AtspiComponent *focus_component = NULL;
-       int i;
-
-       gint nChild;
-       gchar *winName = NULL;
-       gchar *widgetName = NULL;
-       gchar *roleName = NULL;
-
-       if (obj == NULL)
-       {
-               ERROR("No window on top");
-               return;
-       }
-    
-    winName = atspi_accessible_get_name(obj, NULL);
-       ERROR("Win name: %s\n", winName);
+    AtspiAccessible *current_widget = NULL;
+    AtspiStateSet *state_set = NULL;
+    AtspiComponent *focus_component = NULL;
+    int i;
 
-       nChild = atspi_accessible_get_child_count(obj, NULL);
-       ERROR("Child count of top win: %d\n", nChild);
+    gint nChild;
+    gchar *winName = NULL;
+    gchar *widgetName = NULL;
+    gchar *roleName = NULL;
 
-       for (i = 0; i < nChild; i++)
-       {
-               current_widget = atspi_accessible_get_child_at_index(obj, i, NULL);
-               widgetName = atspi_accessible_get_name(current_widget, NULL);
-               ERROR("Widget name: %s\n", widgetName);
+    if (obj == NULL)
+    {
+        ERROR("No window on top");
+        return;
+    }
 
-               roleName = atspi_accessible_get_role_name(current_widget, NULL);
-               ERROR("Widget role: %s\n", roleName);
+    winName = atspi_accessible_get_name(obj, NULL);
+    ERROR("Win name: %s\n", winName);
 
-               state_set = atspi_accessible_get_state_set(current_widget);
-               if (ATSPI_IS_STATE_SET(state_set))
-               {
+    nChild = atspi_accessible_get_child_count(obj, NULL);
+    ERROR("Child count of top win: %d\n", nChild);
 
-                       if (atspi_state_set_contains(ATSPI_STATE_SET(state_set),ATSPI_STATE_FOCUSABLE))
-                       {
-                               ERROR("FOCUSABLE widget name: %s \n", widgetName);
+    for (i = 0; i < nChild; i++)
+    {
+        current_widget = atspi_accessible_get_child_at_index(obj, i, NULL);
+        widgetName = atspi_accessible_get_name(current_widget, NULL);
+        ERROR("Widget name: %s\n", widgetName);
 
-                               current_obj = current_widget;
+        roleName = atspi_accessible_get_role_name(current_widget, NULL);
+        ERROR("Widget role: %s\n", roleName);
 
-                               focus_component = atspi_accessible_get_component(current_widget);
-                               if (focus_component != NULL)
-                               {
-                                       ERROR("Focus component is not null\n");
-                                       if (atspi_component_grab_highlight(focus_component, NULL))
-                                               ERROR("Highlighted!\n");
-                                       return;
-                               }
-                       }
-               }
-       }
+        state_set = atspi_accessible_get_state_set(current_widget);
+        if (ATSPI_IS_STATE_SET(state_set))
+        {
 
-       ERROR("Not found any focusable widget");
+            focus_component = atspi_accessible_get_component(current_widget);
+            if (focus_component != NULL)
+            {
+                ERROR("Focus component is not null\n");
+                if (atspi_component_grab_highlight(focus_component, NULL))
+                    ERROR("Highlighted!\n");
+                return;
+            }
+        }
+    }
+
+    ERROR("Not found any focusable widget");
 
-       return;
+    return;
 }
 
+static void
+_on_cache_builded(void *data)
+{
+   DEBUG("Cache builded");
+   _window_cache_builded = EINA_TRUE;
+}
 
 static void on_window_activate(void *data, AtspiAccessible *window)
 {
@@ -652,13 +829,27 @@ static void on_window_activate(void *data, AtspiAccessible *window)
       if(top_window)
       {
           ERROR("Window name: %s", atspi_accessible_get_name(window, NULL));
-          _get_first_focusable_widget(top_window);
+          current_obj = pivot_chooser_pivot_get(top_window);
+          if (!current_obj)
+          {
+              ERROR("Unable to find pivot widget");
+              return;
+          }
+          else
+            {
+               DEBUG("Found pivot widget: name: %s, role: %s",
+                     atspi_accessible_get_name(current_obj, NULL),
+                     atspi_accessible_get_role_name(current_obj, NULL));
+            }
+         _window_cache_builded = EINA_FALSE;
+         object_cache_build_async(window, 5, _on_cache_builded, NULL);
       }
       else
       {
           ERROR("No top window found!");
           top_window = NULL;
           current_obj = NULL;
+          scrolled_obj = NULL;
       }
 }
 
diff --git a/src/object_cache.c b/src/object_cache.c
new file mode 100644 (file)
index 0000000..9dd1821
--- /dev/null
@@ -0,0 +1,220 @@
+#include <Eina.h>
+#include <Ecore.h>
+
+#include "object_cache.h"
+#include "logger.h"
+
+static Eina_Hash *cache;
+static Ecore_Idler *idler;
+static Eina_List *toprocess;
+static void *user_data;
+static ObjectCacheReadyCb callback;
+static int bulk;
+
+static void
+_cache_item_free_cb(void *data)
+{
+   ObjectCache *co = data;
+   g_free(co->bounds);
+   free(co);
+}
+
+static void
+_list_obj_unref_and_free(Eina_List *toprocess)
+{
+   AtspiAccessible *obj;
+   EINA_LIST_FREE(toprocess, obj)
+      g_object_unref(obj);
+}
+
+static void
+_object_cache_free_internal(void)
+{
+   if (idler)
+     {
+        ecore_idler_del(idler);
+        idler = NULL;
+     }
+   if (toprocess)
+     {
+        _list_obj_unref_and_free(toprocess);
+        toprocess = NULL;
+     }
+   if (cache)
+     {
+        eina_hash_free(cache);
+        cache = NULL;
+     }
+}
+
+/**
+ * Returnes a list of all accessible object implementing AtkCompoment interface
+ * Eina_List should be free with eina_list_free function.
+ * Every AtspiAccessible in list should be unrefed with g_object_unref.
+ */
+static Eina_List*
+_cache_candidates_list_prepare(AtspiAccessible *root)
+{
+   Eina_List *toprocess = NULL, *ret = NULL;
+   int n, i;
+
+   if (!root) return NULL;
+
+   // Keep ref counter +1 on every object in returned list
+   g_object_ref(root);
+   toprocess = eina_list_append(toprocess, root);
+
+   while (toprocess)
+     {
+        AtspiAccessible *obj = eina_list_data_get(toprocess);
+        toprocess = eina_list_remove_list(toprocess, toprocess);
+        n = atspi_accessible_get_child_count(obj, NULL);
+        for (i = 0; i < n; i++)
+          {
+             AtspiAccessible *cand = atspi_accessible_get_child_at_index(obj, i, NULL);
+             toprocess = eina_list_append(toprocess, cand);
+          }
+        ret = eina_list_append(ret, obj);
+     }
+
+   return ret;
+}
+
+static ObjectCache*
+_cache_item_construct(AtspiAccessible *obj)
+{
+   ObjectCache *ret;
+   AtspiComponent *comp;
+
+   ret = calloc(1, sizeof(ObjectCache));
+   if (!ret)
+     {
+        ERROR("out-of memory");
+        return NULL;
+     }
+
+   if ((comp = atspi_accessible_get_component(obj)) != NULL)
+      ret->bounds = atspi_component_get_extents(comp, ATSPI_COORD_TYPE_SCREEN, NULL);
+
+   return ret;
+}
+
+static Eina_List*
+_cache_item_n_cache(Eina_List *objs, int count)
+{
+   int i = 0;
+   Eina_List *ret = objs;
+   ObjectCache *oc;
+   AtspiAccessible *obj;
+
+   for (i = 0; (i < count) && ret; i++)
+     {
+        obj = eina_list_data_get(ret);
+        ret = eina_list_remove_list(ret, ret);
+
+        oc = _cache_item_construct(obj);
+        if (!oc) break;
+
+        eina_hash_add(cache, obj, oc);
+        g_object_unref(obj);
+     }
+
+   return ret;
+}
+
+static Eina_Hash*
+_object_cache_new(void)
+{
+   return eina_hash_pointer_new(_cache_item_free_cb);
+}
+
+void
+object_cache_build(AtspiAccessible *root)
+{
+   Eina_List *objs;
+
+   _object_cache_free_internal();
+   cache = _object_cache_new();
+   if (!cache)
+     {
+        ERROR("ObjectCache: hash table creation failed");
+        return;
+     }
+
+   objs = _cache_candidates_list_prepare(root);
+   _cache_item_n_cache(objs, eina_list_count(objs));
+
+   return;
+}
+
+static Eina_Bool
+_do_cache(void *data)
+{
+   toprocess = _cache_item_n_cache(toprocess, bulk);
+   idler = NULL;
+
+   if (toprocess)
+     idler = ecore_idler_add(_do_cache, NULL);
+   else if (callback)
+     {
+        callback(user_data);
+     }
+
+   return EINA_FALSE;
+}
+
+void
+object_cache_build_async(AtspiAccessible *root, int bulk_size, ObjectCacheReadyCb cb, void *ud)
+{
+   Eina_List *objs;
+
+   _object_cache_free_internal();
+
+   callback = cb;
+   user_data = ud;
+   bulk = bulk_size;
+
+   cache = _object_cache_new();
+   if (!cache)
+     {
+        ERROR("ObjectCache: hash table creation failed");
+        return;
+     }
+
+   objs = _cache_candidates_list_prepare(root);
+   idler = ecore_idler_add(_do_cache, objs);
+
+   return;
+}
+
+void
+object_cache_shutdown(void)
+{
+   _object_cache_free_internal();
+}
+
+const ObjectCache*
+object_cache_get(AtspiAccessible *obj)
+{
+   ObjectCache *ret = NULL;
+   if (!cache)
+     {
+        cache = _object_cache_new();
+        if (!cache)
+          {
+             ERROR("ObjectCache: hash table creation failed");
+             return NULL;
+          }
+     }
+   else
+      ret = eina_hash_find(cache, obj);
+
+   if (!ret && !idler)
+     {
+        // fallback to blocking d-bus call
+        ret = _cache_item_construct(obj);
+        eina_hash_add(cache, obj, ret);
+     }
+
+   return ret;
+}
diff --git a/src/pivot_chooser.c b/src/pivot_chooser.c
new file mode 100644 (file)
index 0000000..ea7edd2
--- /dev/null
@@ -0,0 +1,129 @@
+#include <atspi/atspi.h>
+#include "logger.h"
+#include <Eina.h>
+
+
+/**
+ * @brief Finds first leaf in object hierarchy with given states,
+ * starting from object given as parent.
+ *
+ * This heuristic assumes that focused element have focused
+ * parent widgets.
+ */
+static AtspiAccessible*
+_pivot_with_state_top_down_find(AtspiAccessible *parent, AtspiStateType type)
+{
+   AtspiAccessible *ret = NULL;
+   AtspiStateSet *states;
+   int i;
+
+   states = atspi_accessible_get_state_set(parent);
+   if (!states || atspi_state_set_contains(states, type))
+     {
+        int n = atspi_accessible_get_child_count(parent, NULL);
+        if (n == 0) ret = parent;
+        for (i = 0; i < n; i++)
+          {
+             AtspiAccessible *child = atspi_accessible_get_child_at_index(parent, i, NULL);
+             if (!child) continue;
+
+             ret = _pivot_with_state_top_down_find(child, type);
+
+             g_object_unref(child);
+
+             if (ret) break;
+          }
+     }
+   g_object_unref(states);
+
+   return ret;
+}
+
+/**
+ * @brief Finds first leaf descendant of given object with state @p type
+ */
+static AtspiAccessible*
+_pivot_with_state_flat_find(AtspiAccessible *parent, AtspiStateType type)
+{
+   Eina_List *candidates = NULL, *queue = NULL;
+
+   // ref object to keep same ref count
+   g_object_ref(parent);
+   queue = eina_list_append(queue, parent);
+
+   while (queue)
+     {
+        AtspiAccessible *obj = eina_list_data_get(queue);
+        queue = eina_list_remove_list(queue, queue);
+
+        int n = atspi_accessible_get_child_count(obj, NULL);
+        if (n == 0)
+          candidates = eina_list_append(candidates, obj);
+        else
+          {
+             int i;
+             for (i = 0; i < n; i++)
+               {
+                  AtspiAccessible *child = atspi_accessible_get_child_at_index(obj, i, NULL);
+                  if (child)
+                     queue = eina_list_append(queue, child);
+               }
+             g_object_unref(obj);
+          }
+     }
+
+   // FIXME sort by (x,y) first ??
+   while (candidates)
+     {
+        AtspiAccessible *obj = eina_list_data_get(candidates);
+        candidates = eina_list_remove_list(candidates, candidates);
+
+        AtspiStateSet *states = atspi_accessible_get_state_set(obj);
+        if (states && atspi_state_set_contains(states, type))
+          {
+             g_object_unref(states);
+             g_object_unref(obj);
+             eina_list_free(candidates);
+
+             return obj;
+          }
+
+        g_object_unref(states);
+        g_object_unref(obj);
+     }
+
+   return NULL;
+}
+
+/**
+ * @brief Purpose of this methods is to find first visible object in
+ * hierarchy
+ */
+AtspiAccessible *pivot_chooser_pivot_get(AtspiAccessible *win)
+{
+   AtspiAccessible *ret;
+
+   if (atspi_accessible_get_role(win, NULL) != ATSPI_ROLE_WINDOW)
+     {
+        ERROR("Pivot search entry point must be a Window!");
+        return NULL;
+     }
+
+   DEBUG("Finding SHOWING widget using top-down method.");
+   ret = _pivot_with_state_top_down_find(win, ATSPI_STATE_SHOWING);
+   if (ret) return ret;
+
+   DEBUG("Finding SHOWING widget using top-down method.");
+   ret = _pivot_with_state_flat_find(win, ATSPI_STATE_SHOWING);
+   if (ret) return ret;
+
+   DEBUG("Finding FOCUSED widget using top-down method.");
+   ret = _pivot_with_state_top_down_find(win, ATSPI_STATE_FOCUSED);
+   if (ret) return ret;
+
+   DEBUG("Finding FOCUSED widget using flat search method.");
+   ret = _pivot_with_state_flat_find(win, ATSPI_STATE_FOCUSED);
+   if (ret) return ret;
+
+   return NULL;
+}
diff --git a/src/position_sort.c b/src/position_sort.c
new file mode 100644 (file)
index 0000000..dc084b2
--- /dev/null
@@ -0,0 +1,113 @@
+#include "logger.h"
+#include "object_cache.h"
+#include "position_sort.h"
+
+
+static int
+_sort_by_y_cb(const void *a, const void *b)
+{
+   AtspiAccessible *objA, *objB;
+   const ObjectCache *cA, *cB;
+
+   objA = (AtspiAccessible*)a;
+   objB = (AtspiAccessible*)b;
+
+   cA = object_cache_get(objA);
+   cB = object_cache_get(objB);
+
+   return cA->bounds->y < cB->bounds->y;
+}
+
+static int
+_sort_by_x_cb(const void *a, const void *b)
+{
+   AtspiAccessible *objA, *objB;
+   const ObjectCache *cA, *cB;
+
+   objA = (AtspiAccessible*)a;
+   objB = (AtspiAccessible*)b;
+
+   cA = object_cache_get(objA);
+   cB = object_cache_get(objB);
+
+   return cA->bounds->x < cB->bounds->x;
+}
+
+static Eina_List*
+_get_zones(const Eina_List *objs)
+{
+   Eina_List *candidates = NULL, *l;
+   AtspiAccessible *obj;
+   const ObjectCache *oc;
+
+   EINA_LIST_FOREACH(objs, l, obj)
+     {
+        if (atspi_accessible_get_component(obj))
+          {
+             oc = object_cache_get(obj);
+             // some objects may implement AtspiCompoment interface, however
+             // they do not have valid sizes.
+             if ((oc->bounds->width < 0) || oc->bounds->height < 0)
+               continue;
+             candidates = eina_list_append(candidates, obj);
+          }
+     }
+
+   // Sort object by y - coordinate
+   return eina_list_sort(candidates, 0, _sort_by_y_cb);
+}
+
+static Eina_List*
+_get_lines(const Eina_List *objs)
+{
+   Eina_List *l, *line = NULL, *lines = NULL;
+   AtspiAccessible *obj;
+   const ObjectCache *line_beg;
+
+   EINA_LIST_FOREACH(objs, l, obj)
+     {
+        if (!line) {
+          // set first object in line
+          line = eina_list_append(line, obj);
+          line_beg = object_cache_get(obj);
+          continue;
+        }
+
+        const ObjectCache *oc = object_cache_get(obj);
+        // Object are considered as present in same line, if
+        // its y coordinate begins maximum 25% below
+        // y coordinate of first object in line.
+        if ((line_beg->bounds->y + 0.25 * line_beg->bounds->height) >
+            oc->bounds->y)
+          {
+             line = eina_list_append(line, obj);
+             continue;
+          }
+        lines = eina_list_append(lines, line);
+        line = NULL;
+     }
+
+   return lines;
+}
+
+Eina_List *position_sort(const Eina_List *objs)
+{
+   Eina_List *l, *line, *zones, *lines = NULL;
+
+   // Get list of objects occupying place on the screen
+   zones = _get_zones(objs);
+
+   // Cluster all zones into lines - verticaly
+   lines = _get_lines(zones);
+
+   // sort all zones in line - horizontaly
+   EINA_LIST_FOREACH(lines, l, line)
+     {
+        line = eina_list_sort(line, 0, _sort_by_x_cb);
+        eina_list_data_set(l, line);
+     }
+
+   if (zones) eina_list_free(zones);
+
+   return lines;
+}
diff --git a/src/structural_navi.c b/src/structural_navi.c
new file mode 100644 (file)
index 0000000..6337f7e
--- /dev/null
@@ -0,0 +1,104 @@
+#include "structural_navi.h"
+#include "logger.h"
+
+AtspiAccessible *structural_navi_same_level_next(AtspiAccessible *current)
+{
+    AtspiAccessible *parent;
+    AtspiRole role;
+    int id, n;
+
+    parent = atspi_accessible_get_parent(current, NULL);
+    if (!parent) return NULL;
+
+    role = atspi_accessible_get_role(parent, NULL);
+    if (role != ATSPI_ROLE_DESKTOP_FRAME)
+      {
+         n = atspi_accessible_get_child_count(parent, NULL);
+         id = atspi_accessible_get_index_in_parent(current, NULL);
+         if (id == (n - 1)) DEBUG("End of children structure list reached.");
+         id = (id + 1) == n ? id : id + 1;
+         return atspi_accessible_get_child_at_index(parent, id, NULL);
+      }
+    return NULL;
+}
+
+AtspiAccessible *structural_navi_same_level_prev(AtspiAccessible *current)
+{
+    AtspiAccessible *parent;
+    AtspiRole role;
+    int id;
+
+    parent = atspi_accessible_get_parent(current, NULL);
+    if (!parent) return NULL;
+
+    role = atspi_accessible_get_role(parent, NULL);
+    if (role != ATSPI_ROLE_DESKTOP_FRAME)
+      {
+         id = atspi_accessible_get_index_in_parent(current, NULL);
+         if (id == 0) DEBUG("Begining of children structure list reached.");
+         id = id  == 0 ? id : id - 1;
+         return atspi_accessible_get_child_at_index(parent, id, NULL);
+      }
+    return NULL;
+}
+
+AtspiAccessible *structural_navi_level_up(AtspiAccessible *current)
+{
+    AtspiAccessible *parent;
+    AtspiRole role;
+
+    parent = atspi_accessible_get_parent(current, NULL);
+    if (!parent) return NULL;
+
+    role = atspi_accessible_get_role(parent, NULL);
+    if (role != ATSPI_ROLE_DESKTOP_FRAME)
+      {
+         return parent;
+      }
+    return NULL;
+}
+
+AtspiAccessible *structural_navi_level_down(AtspiAccessible *current)
+{
+    int n = atspi_accessible_get_child_count(current, NULL);
+    if (!n) return NULL;
+
+    return atspi_accessible_get_child_at_index(current, 0, NULL);
+}
+
+static AtspiAccessible*
+_navi_app_chain_next(AtspiAccessible *current, AtspiRelationType search_type)
+{
+   GArray *relations;
+   AtspiAccessible *ret = NULL;
+   AtspiRelation *relation;
+   AtspiRelationType type;
+   int i;
+
+   relations = atspi_accessible_get_relation_set(current, NULL);
+
+    for (i = 0; i < relations->len; i++)
+      {
+         relation = g_array_index (relations, AtspiRelation*, i);
+         type = atspi_relation_get_relation_type(relation);
+
+         if (type == search_type)
+           {
+              ret = atspi_relation_get_target(relation, 0);
+              break;
+           }
+      }
+
+   g_array_free(relations, TRUE);
+   return ret;
+}
+
+AtspiAccessible *structural_navi_app_chain_next(AtspiAccessible *current)
+{
+   return _navi_app_chain_next(current, ATSPI_RELATION_FLOWS_TO);
+}
+
+AtspiAccessible *structural_navi_app_chain_prev(AtspiAccessible *current)
+{
+   return _navi_app_chain_next(current, ATSPI_RELATION_FLOWS_FROM);
+}