// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/gtk/accessibility_event_router_gtk.h"

#include "base/basictypes.h"
#include "base/callback.h"
#include "base/message_loop.h"
#include "base/stl_util-inl.h"
#include "chrome/browser/extensions/extension_accessibility_api.h"
#include "chrome/browser/gtk/gtk_chrome_link_button.h"
#include "chrome/browser/profile.h"
#include "chrome/common/notification_type.h"
#include "views/controls/textfield/native_textfield_gtk.h"

#if defined(TOOLKIT_VIEWS)
#include "views/controls/textfield/gtk_views_textview.h"
#include "views/controls/textfield/gtk_views_entry.h"
#endif

namespace {

//
// Callbacks triggered by signals on gtk widgets.
//

gboolean OnWidgetFocused(GSignalInvocationHint *ihint,
                         guint n_param_values,
                         const GValue* param_values,
                         gpointer user_data) {
  GtkWidget* widget = GTK_WIDGET(g_value_get_object(param_values));
  reinterpret_cast<AccessibilityEventRouterGtk*>(user_data)->
      DispatchAccessibilityNotification(
          widget, NotificationType::ACCESSIBILITY_CONTROL_FOCUSED);
  return TRUE;
}

gboolean OnButtonClicked(GSignalInvocationHint *ihint,
                         guint n_param_values,
                         const GValue* param_values,
                         gpointer user_data) {
  GtkWidget* widget = GTK_WIDGET(g_value_get_object(param_values));
  // Skip toggle buttons because we're also listening on "toggle" events.
  if (GTK_IS_TOGGLE_BUTTON(widget))
    return true;
  reinterpret_cast<AccessibilityEventRouterGtk*>(user_data)->
      DispatchAccessibilityNotification(
          widget, NotificationType::ACCESSIBILITY_CONTROL_ACTION);
  return TRUE;
}

gboolean OnButtonToggled(GSignalInvocationHint *ihint,
                         guint n_param_values,
                         const GValue* param_values,
                         gpointer user_data) {
  GtkWidget* widget = GTK_WIDGET(g_value_get_object(param_values));
  bool checked = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
  // Skip propagating an "uncheck" event for a radio button because it's
  // redundant; there will always be a corresponding "check" event for
  // a different radio button the group.
  if (GTK_IS_RADIO_BUTTON(widget) && !checked)
    return true;
  reinterpret_cast<AccessibilityEventRouterGtk*>(user_data)->
      DispatchAccessibilityNotification(
          widget, NotificationType::ACCESSIBILITY_CONTROL_ACTION);
  return TRUE;
}

gboolean OnPageSwitched(GSignalInvocationHint *ihint,
                        guint n_param_values,
                        const GValue* param_values,
                        gpointer user_data) {
  GtkWidget* widget = GTK_WIDGET(g_value_get_object(param_values));
  // The page hasn't switched yet, so defer calling
  // DispatchAccessibilityNotification.
  reinterpret_cast<AccessibilityEventRouterGtk*>(user_data)->
      PostDispatchAccessibilityNotification(
          widget, NotificationType::ACCESSIBILITY_CONTROL_ACTION);
  return TRUE;
}

gboolean OnComboBoxChanged(GSignalInvocationHint *ihint,
                           guint n_param_values,
                           const GValue* param_values,
                           gpointer user_data) {
  GtkWidget* widget = GTK_WIDGET(g_value_get_object(param_values));
  if (!GTK_IS_COMBO_BOX(widget))
    return true;
  reinterpret_cast<AccessibilityEventRouterGtk*>(user_data)->
      DispatchAccessibilityNotification(
          widget, NotificationType::ACCESSIBILITY_CONTROL_ACTION);
  return TRUE;
}

gboolean OnTreeViewCursorChanged(GSignalInvocationHint *ihint,
                                 guint n_param_values,
                                 const GValue* param_values,
                                 gpointer user_data) {
  GtkWidget* widget = GTK_WIDGET(g_value_get_object(param_values));
  if (!GTK_IS_TREE_VIEW(widget)) {
    return true;
  }
  reinterpret_cast<AccessibilityEventRouterGtk*>(user_data)->
      DispatchAccessibilityNotification(
          widget, NotificationType::ACCESSIBILITY_CONTROL_ACTION);
  return TRUE;
}

gboolean OnEntryChanged(GSignalInvocationHint *ihint,
                        guint n_param_values,
                        const GValue* param_values,
                        gpointer user_data) {
  GtkWidget* widget = GTK_WIDGET(g_value_get_object(param_values));
  if (!GTK_IS_ENTRY(widget)) {
    return TRUE;
  }
  // The text hasn't changed yet, so defer calling
  // DispatchAccessibilityNotification.
  reinterpret_cast<AccessibilityEventRouterGtk*>(user_data)->
      PostDispatchAccessibilityNotification(
          widget, NotificationType::ACCESSIBILITY_TEXT_CHANGED);
  return TRUE;
}

gboolean OnTextBufferChanged(GSignalInvocationHint *ihint,
                             guint n_param_values,
                             const GValue* param_values,
                             gpointer user_data) {
  // The text hasn't changed yet, so defer calling
  // DispatchAccessibilityNotification.
  reinterpret_cast<AccessibilityEventRouterGtk*>(user_data)->
      PostDispatchAccessibilityNotification(
          NULL, NotificationType::ACCESSIBILITY_TEXT_CHANGED);
  return TRUE;
}

gboolean OnTextViewChanged(GSignalInvocationHint *ihint,
                           guint n_param_values,
                           const GValue* param_values,
                           gpointer user_data) {
  GtkWidget* widget = GTK_WIDGET(g_value_get_object(param_values));
  if (!GTK_IS_TEXT_VIEW(widget)) {
    return TRUE;
  }
  // The text hasn't changed yet, so defer calling
  // DispatchAccessibilityNotification.
  reinterpret_cast<AccessibilityEventRouterGtk*>(user_data)->
      PostDispatchAccessibilityNotification(
          widget, NotificationType::ACCESSIBILITY_TEXT_CHANGED);
  return TRUE;
}

gboolean OnMenuMoveCurrent(GSignalInvocationHint *ihint,
                           guint n_param_values,
                           const GValue* param_values,
                           gpointer user_data) {
  // Get the widget (the GtkMenu).
  GtkWidget* widget = GTK_WIDGET(g_value_get_object(param_values));

  // Moving may move us into or out of a submenu, so after the menu
  // item moves, |widget| may not be valid anymore. To be safe, then,
  // find the topmost ancestor of this menu and post the notification
  // dispatch on that menu. Then the dispatcher will recurse into submenus
  // as necessary to figure out which item is focused.
  while (GTK_MENU_SHELL(widget)->parent_menu_shell)
    widget = GTK_MENU_SHELL(widget)->parent_menu_shell;

  // The menu item hasn't moved yet, so we want to defer calling
  // DispatchAccessibilityNotification until after it does.
  reinterpret_cast<AccessibilityEventRouterGtk*>(user_data)->
      PostDispatchAccessibilityNotification(
          widget, NotificationType::ACCESSIBILITY_CONTROL_FOCUSED);
  return TRUE;
}

}  // anonymous namespace

AccessibilityEventRouterGtk::AccessibilityEventRouterGtk()
    : listening_(false),
      most_recent_profile_(NULL),
      most_recent_widget_(NULL),
      method_factory_(this) {
  // We don't want our event listeners to be installed if accessibility is
  // disabled. Install listeners so we can install and uninstall them as
  // needed, then install them now if it's currently enabled.
  ExtensionAccessibilityEventRouter *extension_event_router =
      ExtensionAccessibilityEventRouter::GetInstance();
  extension_event_router->AddOnEnabledListener(
      NewCallback(this,
                  &AccessibilityEventRouterGtk::InstallEventListeners));
  extension_event_router->AddOnDisabledListener(
      NewCallback(this,
                  &AccessibilityEventRouterGtk::RemoveEventListeners));
  if (extension_event_router->IsAccessibilityEnabled()) {
    InstallEventListeners();
  }
}

AccessibilityEventRouterGtk::~AccessibilityEventRouterGtk() {
  RemoveEventListeners();
}

// static
AccessibilityEventRouterGtk* AccessibilityEventRouterGtk::GetInstance() {
  return Singleton<AccessibilityEventRouterGtk>::get();
}

void AccessibilityEventRouterGtk::InstallEventListener(
    const char* signal_name,
    GType widget_type,
    GSignalEmissionHook hook_func) {
  guint signal_id = g_signal_lookup(signal_name, widget_type);
  gulong hook_id = g_signal_add_emission_hook(
      signal_id, 0, hook_func, reinterpret_cast<gpointer>(this), NULL);
  installed_hooks_.push_back(InstalledHook(signal_id, hook_id));
}

bool AccessibilityEventRouterGtk::IsPassword(GtkWidget* widget) {
  bool is_password = false;
#if defined (TOOLKIT_VIEWS)
  is_password = (GTK_IS_VIEWS_ENTRY(widget) &&
                 GTK_VIEWS_ENTRY(widget)->host != NULL &&
                 GTK_VIEWS_ENTRY(widget)->host->IsPassword()) ||
                (GTK_IS_VIEWS_TEXTVIEW(widget) &&
                 GTK_VIEWS_TEXTVIEW(widget)->host != NULL &&
                 GTK_VIEWS_TEXTVIEW(widget)->host->IsPassword());
#endif
  return is_password;
}


void AccessibilityEventRouterGtk::InstallEventListeners() {
  // Create and destroy each type of widget we need signals for,
  // to ensure their modules are loaded, otherwise g_signal_lookup
  // might fail.
  g_object_unref(g_object_ref_sink(gtk_combo_box_new()));
  g_object_unref(g_object_ref_sink(gtk_entry_new()));
  g_object_unref(g_object_ref_sink(gtk_notebook_new()));
  g_object_unref(g_object_ref_sink(gtk_toggle_button_new()));
  g_object_unref(g_object_ref_sink(gtk_tree_view_new()));
  g_object_unref(g_object_ref_sink(gtk_text_view_new()));
  g_object_unref(g_object_ref_sink(gtk_text_buffer_new(NULL)));

  // Add signal emission hooks for the events we're interested in.
  InstallEventListener("clicked", GTK_TYPE_BUTTON, OnButtonClicked);
  InstallEventListener("changed", GTK_TYPE_COMBO_BOX, OnComboBoxChanged);
  InstallEventListener("cursor-changed", GTK_TYPE_TREE_VIEW,
                       OnTreeViewCursorChanged);
  InstallEventListener("changed", GTK_TYPE_ENTRY, OnEntryChanged);
  InstallEventListener("insert-text", GTK_TYPE_ENTRY, OnEntryChanged);
  InstallEventListener("delete-text", GTK_TYPE_ENTRY, OnEntryChanged);
  InstallEventListener("move-cursor", GTK_TYPE_ENTRY, OnEntryChanged);
  InstallEventListener("focus-in-event", GTK_TYPE_WIDGET, OnWidgetFocused);
  InstallEventListener("switch-page", GTK_TYPE_NOTEBOOK, OnPageSwitched);
  InstallEventListener("toggled", GTK_TYPE_TOGGLE_BUTTON, OnButtonToggled);
  InstallEventListener("move-current", GTK_TYPE_MENU, OnMenuMoveCurrent);
  InstallEventListener("changed", GTK_TYPE_TEXT_BUFFER, OnTextBufferChanged);
  InstallEventListener("move-cursor", GTK_TYPE_TEXT_VIEW, OnTextViewChanged);

  listening_ = true;
}

void AccessibilityEventRouterGtk::RemoveEventListeners() {
  for (size_t i = 0; i < installed_hooks_.size(); i++) {
    g_signal_remove_emission_hook(
        installed_hooks_[i].signal_id,
        installed_hooks_[i].hook_id);
  }
  installed_hooks_.clear();

  listening_ = false;
}

void AccessibilityEventRouterGtk::AddRootWidget(
    GtkWidget* root_widget, Profile* profile) {
  root_widget_info_map_[root_widget].refcount++;
  root_widget_info_map_[root_widget].profile = profile;
}

void AccessibilityEventRouterGtk::RemoveRootWidget(GtkWidget* root_widget) {
  DCHECK(root_widget_info_map_.find(root_widget) !=
         root_widget_info_map_.end());
  root_widget_info_map_[root_widget].refcount--;
  if (root_widget_info_map_[root_widget].refcount == 0) {
    root_widget_info_map_.erase(root_widget);
  }
}

void AccessibilityEventRouterGtk::AddWidgetNameOverride(
    GtkWidget* widget, std::string name) {
  widget_info_map_[widget].name = name;
  widget_info_map_[widget].refcount++;
}

void AccessibilityEventRouterGtk::RemoveWidgetNameOverride(GtkWidget* widget) {
  DCHECK(widget_info_map_.find(widget) != widget_info_map_.end());
  widget_info_map_[widget].refcount--;
  if (widget_info_map_[widget].refcount == 0) {
    widget_info_map_.erase(widget);
  }
}

void AccessibilityEventRouterGtk::FindWidget(
    GtkWidget* widget, Profile** profile, bool* is_accessible) {
  *is_accessible = false;

  for (base::hash_map<GtkWidget*, RootWidgetInfo>::const_iterator iter =
           root_widget_info_map_.begin();
       iter != root_widget_info_map_.end();
       ++iter) {
    if (widget == iter->first || gtk_widget_is_ancestor(widget, iter->first)) {
      *is_accessible = true;
      if (profile)
        *profile = iter->second.profile;
      break;
    }
  }
}

std::string AccessibilityEventRouterGtk::GetWidgetName(GtkWidget* widget) {
  base::hash_map<GtkWidget*, WidgetInfo>::const_iterator iter =
      widget_info_map_.find(widget);
  if (iter != widget_info_map_.end()) {
    return iter->second.name;
  } else {
    return "";
  }
}

void AccessibilityEventRouterGtk::StartListening() {
  listening_ = true;
}

void AccessibilityEventRouterGtk::StopListening() {
  listening_ = false;
}

void AccessibilityEventRouterGtk::DispatchAccessibilityNotification(
    GtkWidget* widget, NotificationType type) {
  // If there's no message loop, we must be about to shutdown or we're
  // running inside a test; either way, there's no reason to do any
  // further processing.
  if (!MessageLoop::current())
    return;

  if (!listening_)
    return;

  Profile* profile = NULL;
  bool is_accessible;

  // Special case: when we get ACCESSIBILITY_TEXT_CHANGED, we don't get
  // a pointer to the widget, so we try to retrieve it from the most recent
  // widget.
  if (widget == NULL &&
      type == NotificationType::ACCESSIBILITY_TEXT_CHANGED &&
      most_recent_widget_ &&
      GTK_IS_TEXT_VIEW(most_recent_widget_)) {
    widget = most_recent_widget_;
  }

  if (!widget)
    return;

  most_recent_widget_ = widget;
  FindWidget(widget, &profile, &is_accessible);
  if (profile)
    most_recent_profile_ = profile;

  // Special case: a GtkMenu isn't associated with any particular
  // toplevel window, so menu events get routed to the profile of
  // the most recent event that was associated with a window.
  if (GTK_IS_MENU_SHELL(widget) && most_recent_profile_) {
    SendMenuItemNotification(widget, type, most_recent_profile_);
    return;
  }

  // In all other cases, return if this widget wasn't marked as accessible.
  if (!is_accessible)
    return;

  // The order of these checks matters, because, for example, a radio button
  // is a subclass of button, and a combo box is a composite control where
  // the focus event goes to the button that's a child of the combo box.
  GtkWidget* parent = gtk_widget_get_parent(widget);
  if (parent && GTK_IS_BUTTON(widget) && GTK_IS_TREE_VIEW(parent)) {
    // This is a list box column header.  Currently not supported.
    return;
  } else if (GTK_IS_COMBO_BOX(widget)) {
    SendComboBoxNotification(widget, type, profile);
  } else if (parent && GTK_IS_COMBO_BOX(parent)) {
    SendComboBoxNotification(parent, type, profile);
  } else if (GTK_IS_RADIO_BUTTON(widget)) {
    SendRadioButtonNotification(widget, type, profile);
  } else if (GTK_IS_TOGGLE_BUTTON(widget)) {
    SendCheckboxNotification(widget, type, profile);
  } else if (GTK_IS_BUTTON(widget)) {
    SendButtonNotification(widget, type, profile);
  } else if (GTK_IS_ENTRY(widget)) {
    SendEntryNotification(widget, type, profile);
  } else if (GTK_IS_TEXT_VIEW(widget)) {
    SendTextViewNotification(widget, type, profile);
  } else if (GTK_IS_NOTEBOOK(widget)) {
    SendTabNotification(widget, type, profile);
  } else if (GTK_IS_TREE_VIEW(widget)) {
    SendListBoxNotification(widget, type, profile);
  } else {
    // If we have no idea what this control is, return and skip the
    // temporary pause in event listening.
    return;
  }

  // After this method returns, additional signal handlers will run,
  // which will sometimes generate additional signals.  To avoid
  // generating redundant accessibility notifications for the same
  // initial event, stop listening to all signals generated from now
  // until this posted task runs.
  StopListening();
  MessageLoop::current()->PostTask(
      FROM_HERE, method_factory_.NewRunnableMethod(
          &AccessibilityEventRouterGtk::StartListening));
}

void AccessibilityEventRouterGtk::PostDispatchAccessibilityNotification(
    GtkWidget* widget, NotificationType type) {
  if (!MessageLoop::current())
    return;

  MessageLoop::current()->PostTask(
      FROM_HERE, method_factory_.NewRunnableMethod(
          &AccessibilityEventRouterGtk::DispatchAccessibilityNotification,
          widget,
          type));
}

void AccessibilityEventRouterGtk::SendRadioButtonNotification(
    GtkWidget* widget, NotificationType type, Profile* profile) {
  // Get the radio button name
  std::string button_name = GetWidgetName(widget);
  if (button_name.empty() && gtk_button_get_label(GTK_BUTTON(widget)))
    button_name = gtk_button_get_label(GTK_BUTTON(widget));

  // Get its state
  bool checked = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));

  // Get the index of this radio button and the total number of
  // radio buttons in the group.
  int item_count = 0;
  int item_index = -1;
  for (GSList* group = gtk_radio_button_get_group(GTK_RADIO_BUTTON(widget));
       group;
       group = group->next) {
    if (group->data == widget) {
      item_index = item_count;
    }
    item_count++;
  }
  item_index = item_count - 1 - item_index;

  AccessibilityRadioButtonInfo info(
      profile, button_name, checked, item_index, item_count);
  SendAccessibilityNotification(type, &info);
}

void AccessibilityEventRouterGtk::SendCheckboxNotification(
    GtkWidget* widget, NotificationType type, Profile* profile) {
  std::string button_name = GetWidgetName(widget);
  if (button_name.empty() && gtk_button_get_label(GTK_BUTTON(widget)))
    button_name = gtk_button_get_label(GTK_BUTTON(widget));
  bool checked = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
  AccessibilityCheckboxInfo info(profile, button_name, checked);
  SendAccessibilityNotification(type, &info);
}

void AccessibilityEventRouterGtk::SendButtonNotification(
    GtkWidget* widget, NotificationType type, Profile* profile) {
  std::string button_name = GetWidgetName(widget);
  if (button_name.empty() && gtk_button_get_label(GTK_BUTTON(widget)))
    button_name = gtk_button_get_label(GTK_BUTTON(widget));
  AccessibilityButtonInfo info(profile, button_name);
  SendAccessibilityNotification(type, &info);
}

void AccessibilityEventRouterGtk::SendEntryNotification(
    GtkWidget* widget, NotificationType type, Profile* profile) {
  std::string name = GetWidgetName(widget);
  std::string value = gtk_entry_get_text(GTK_ENTRY(widget));
  gint start_pos;
  gint end_pos;
  gtk_editable_get_selection_bounds(GTK_EDITABLE(widget), &start_pos, &end_pos);
  AccessibilityTextBoxInfo info(profile, name, IsPassword(widget));
  info.SetValue(value, start_pos, end_pos);
  SendAccessibilityNotification(type, &info);
}

void AccessibilityEventRouterGtk::SendTextViewNotification(
    GtkWidget* widget, NotificationType type, Profile* profile) {
  std::string name = GetWidgetName(widget);
  GtkTextBuffer* buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget));
  GtkTextIter start, end;
  gtk_text_buffer_get_bounds(buffer, &start, &end);
  gchar* text = gtk_text_buffer_get_text(buffer, &start, &end, false);
  std::string value = text;
  g_free(text);
  GtkTextIter sel_start, sel_end;
  gtk_text_buffer_get_selection_bounds(buffer, &sel_start, &sel_end);
  int start_pos = gtk_text_iter_get_offset(&sel_start);
  int end_pos = gtk_text_iter_get_offset(&sel_end);
  AccessibilityTextBoxInfo info(profile, name, IsPassword(widget));
  info.SetValue(value, start_pos, end_pos);
  SendAccessibilityNotification(type, &info);
}

void AccessibilityEventRouterGtk::SendTabNotification(
    GtkWidget* widget, NotificationType type, Profile* profile) {
  int index = gtk_notebook_get_current_page(GTK_NOTEBOOK(widget));
  int page_count = gtk_notebook_get_n_pages(GTK_NOTEBOOK(widget));
  GtkWidget* page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(widget), index);
  GtkWidget* label = gtk_notebook_get_tab_label(GTK_NOTEBOOK(widget), page);
  std::string name = GetWidgetName(widget);
  if (name.empty() && gtk_label_get_text(GTK_LABEL(label))) {
    name = gtk_label_get_text(GTK_LABEL(label));
  }
  AccessibilityTabInfo info(profile, name, index, page_count);
  SendAccessibilityNotification(type, &info);
}

void AccessibilityEventRouterGtk::SendComboBoxNotification(
    GtkWidget* widget, NotificationType type, Profile* profile) {
  // Get the index of the selected item.  Will return -1 if no item is
  // active, which matches the semantics of the extension API.
  int index = gtk_combo_box_get_active(GTK_COMBO_BOX(widget));

  // Get the number of items.
  GtkTreeModel* model = gtk_combo_box_get_model(GTK_COMBO_BOX(widget));
  int count = gtk_tree_model_iter_n_children(model, NULL);

  // Get the value of the current item, if possible.  Note that the
  // model behind the combo box could be arbitrarily complex in theory,
  // but this code just handles flat lists where the first string column
  // contains the display value.
  std::string value;
  int string_column_index = -1;
  for (int i = 0; i < gtk_tree_model_get_n_columns(model); i++) {
    if (gtk_tree_model_get_column_type(model, i) == G_TYPE_STRING) {
      string_column_index = i;
      break;
    }
  }
  if (string_column_index) {
    GtkTreeIter iter;
    if (gtk_combo_box_get_active_iter(GTK_COMBO_BOX(widget), &iter)) {
      GValue gvalue = { 0 };
      gtk_tree_model_get_value(model, &iter, string_column_index, &gvalue);
      const char* string_value = g_value_get_string(&gvalue);
      if (string_value) {
        value = string_value;
      }
      g_value_unset(&gvalue);
    }
  } else {
    // Otherwise this must be a gtk_combo_box_text, in which case this
    // function will return the value of the current item, instead.
    value = gtk_combo_box_get_active_text(GTK_COMBO_BOX(widget));
  }

  // Get the name of this combo box.
  std::string name = GetWidgetName(widget);

  // Send the notification.
  AccessibilityComboBoxInfo info(profile, name, value, index, count);
  SendAccessibilityNotification(type, &info);
}

void AccessibilityEventRouterGtk::SendListBoxNotification(
    GtkWidget* widget, NotificationType type, Profile* profile) {
  // Get the number of items.
  GtkTreeModel* model = gtk_tree_view_get_model(GTK_TREE_VIEW(widget));
  int count = gtk_tree_model_iter_n_children(model, NULL);

  // Get the current selected index and its value.
  int index = -1;
  std::string value;
  GtkTreePath* path;
  gtk_tree_view_get_cursor(GTK_TREE_VIEW(widget), &path, NULL);
  if (path != NULL) {
    gint* indices = gtk_tree_path_get_indices(path);
    if (indices)
      index = indices[0];

    GtkTreeIter iter;
    if (gtk_tree_model_get_iter(model, &iter, path)) {
      for (int i = 0; i < gtk_tree_model_get_n_columns(model); i++) {
        if (gtk_tree_model_get_column_type(model, i) == G_TYPE_STRING) {
          GValue gvalue = { 0 };
          gtk_tree_model_get_value(model, &iter, i, &gvalue);
          const char* string_value = g_value_get_string(&gvalue);
          if (string_value) {
            if (!value.empty())
              value += " ";
            value += string_value;
          }
          g_value_unset(&gvalue);
        }
      }
    }

    gtk_tree_path_free(path);
  }

  // Get the name of this control.
  std::string name = GetWidgetName(widget);

  // Send the notification.
  AccessibilityListBoxInfo info(profile, name, value, index, count);
  SendAccessibilityNotification(type, &info);
}

void AccessibilityEventRouterGtk::SendMenuItemNotification(
    GtkWidget* menu, NotificationType type, Profile* profile) {
  // Find the focused menu item, recursing into submenus as needed.
  GtkWidget* menu_item = GTK_MENU_SHELL(menu)->active_menu_item;
  if (!menu_item)
    return;
  GtkWidget* submenu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(menu_item));
  while (submenu && GTK_MENU_SHELL(submenu)->active_menu_item) {
    menu = submenu;
    menu_item = GTK_MENU_SHELL(menu)->active_menu_item;
    if (!menu_item)
      return;
    submenu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(menu_item));
  }

  // Figure out the item index and total number of items.
  GList* items = gtk_container_get_children(GTK_CONTAINER(menu));
  guint count = g_list_length(items);
  int index = g_list_index(items, static_cast<gconstpointer>(menu_item));

  // Get the menu item's label.
  std::string name;
#if GTK_CHECK_VERSION(2, 16, 0)
  name = gtk_menu_item_get_label(GTK_MENU_ITEM(menu_item));
#else
  GList* children = gtk_container_get_children(GTK_CONTAINER(menu_item));
  for (GList* l = g_list_first(children); l != NULL; l = g_list_next(l)) {
    GtkWidget* child = static_cast<GtkWidget*>(l->data);
    if (GTK_IS_LABEL(child)) {
      name = gtk_label_get_label(GTK_LABEL(child));
      break;
    }
  }
#endif

  // Send the event.
  AccessibilityMenuItemInfo info(profile, name, submenu != NULL, index, count);
  SendAccessibilityNotification(type, &info);
}