diff options
author | dmazzoni@chromium.org <dmazzoni@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-09-30 14:14:29 +0000 |
---|---|---|
committer | dmazzoni@chromium.org <dmazzoni@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-09-30 14:14:29 +0000 |
commit | 414591b421922b06e50c99fd41cd6ed2033a21ad (patch) | |
tree | a87c7961fd5f834f68d1babe1d87adbe77366b62 | |
parent | e3fe59bfb0912596e8975f9c50b01683c19ee5d9 (diff) | |
download | chromium_src-414591b421922b06e50c99fd41cd6ed2033a21ad.zip chromium_src-414591b421922b06e50c99fd41cd6ed2033a21ad.tar.gz chromium_src-414591b421922b06e50c99fd41cd6ed2033a21ad.tar.bz2 |
Add detailed speech descriptions for Chrome OS login screen accessibility
events.
BUG=none
TEST=Added new unittest: WizardAccessibilityHandlerTest
Review URL: http://codereview.chromium.org/3223011
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@61062 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | chrome/app/generated_resources.grd | 77 | ||||
-rw-r--r-- | chrome/browser/accessibility_events.h | 26 | ||||
-rw-r--r-- | chrome/browser/chromeos/login/wizard_accessibility_handler.cc | 431 | ||||
-rw-r--r-- | chrome/browser/chromeos/login/wizard_accessibility_handler.h | 73 | ||||
-rw-r--r-- | chrome/browser/chromeos/login/wizard_accessibility_handler_unittest.cc | 164 | ||||
-rw-r--r-- | chrome/browser/chromeos/login/wizard_accessibility_helper.cc | 4 | ||||
-rw-r--r-- | chrome/browser/chromeos/login/wizard_accessibility_helper.h | 4 | ||||
-rw-r--r-- | chrome/browser/chromeos/login/wizard_controller.cc | 4 | ||||
-rw-r--r-- | chrome/browser/extensions/extension_accessibility_api.cc | 6 | ||||
-rw-r--r-- | chrome/browser/views/accessible_view_helper.cc | 32 | ||||
-rw-r--r-- | chrome/browser/views/accessible_view_helper.h | 4 | ||||
-rw-r--r-- | chrome/chrome_tests.gypi | 1 |
12 files changed, 800 insertions, 26 deletions
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd index 2e90870..a48b749 100644 --- a/chrome/app/generated_resources.grd +++ b/chrome/app/generated_resources.grd @@ -9480,6 +9480,83 @@ Keep your key file in a safe place. You will need it to create new versions of y <message name="IDS_PROXY_PORT" desc="Label for the Port box."> Port </message> + + <!-- Accessibility strings, to be spoken --> + <message name="IDS_CHROMEOS_ACC_BUTTON" desc="Label indicating that a control is a pushbutton."> + Button + </message> + <message name="IDS_CHROMEOS_ACC_CHECKBOX_UNCHECKED" desc="Label indicating that a control is an unchecked check box."> + Unchecked check box + </message> + <message name="IDS_CHROMEOS_ACC_CHECKBOX_CHECKED" desc="Label indicating that a control is a checked check box."> + Checked check box + </message> + <message name="IDS_CHROMEOS_ACC_RADIO_UNSELECTED" desc="Label indicating that a control is an unselected radio button."> + Unselected radio button + </message> + <message name="IDS_CHROMEOS_ACC_RADIO_SELECTED" desc="Label indicating that a control is a selected radio button."> + Selected radio button + </message> + <message name="IDS_CHROMEOS_ACC_COMBOBOX" desc="Label indicating that a control is a combo box, a control that pops down a list of options to choose from."> + Combo box + </message> + <message name="IDS_CHROMEOS_ACC_LISTBOX" desc="Label indicating that a control is a list box, a control that shows a fixed list of options to choose from."> + List box + </message> + <message name="IDS_CHROMEOS_ACC_LINK" desc="Label indicating that a control is a clickable link."> + Link + </message> + <message name="IDS_CHROMEOS_ACC_MENU" desc="Label indicating that a control is a menu that will drop down to reveal options if you click on it."> + Menu + </message> + <message name="IDS_CHROMEOS_ACC_TAB" desc="Label indicating that a control is one tab in a tabbed dialog or window."> + Tab + </message> + <message name="IDS_CHROMEOS_ACC_TEXTBOX" desc="Label indicating that a control is an editable text box."> + Text box + </message> + <message name="IDS_CHROMEOS_ACC_PASSWORDBOX" desc="Label indicating that a control is a secure editable text box for a password, where the keys you type will be obscured."> + Password text box + </message> + <message name="IDS_CHROMEOS_ACC_INDEX_OF_COUNT" desc="Phrase indicating that the current selected item is number INDEX out of a list of COUNT choices, for example if the list is A, B, C, D, E and the user has selected C the phrase would be '3 of 5'."> + <ph name="INDEX">$1<ex>3</ex></ph> of <ph name="COUNT">$2<ex>5</ex></ph> + </message> + <message name="IDS_CHROMEOS_ACC_HAS_SUBMENU" desc="Phrase indicating that a menu item has a submenu."> + Has submenu + </message> + <message name="IDS_CHROMEOS_ACC_SELECTED" desc="Phrase indicating that a control was selected or activated."> + Selected + </message> + <message name="IDS_CHROMEOS_ACC_EXCLAMATION_POINT" desc="The name of the exclamation point character."> + Exclamation point + </message> + <message name="IDS_CHROMEOS_ACC_LEFT_PAREN" desc="The name of the left parenthesis character, abbreviated or shortened if possible."> + Left paren + </message> + <message name="IDS_CHROMEOS_ACC_RIGHT_PAREN" desc="The name of the left parenthesis character, abbreviated or shortened if possible."> + Right paren + </message> + <message name="IDS_CHROMEOS_ACC_SEMICOLON" desc="The name of the semicolon character."> + Semicolon + </message> + <message name="IDS_CHROMEOS_ACC_COLON" desc="The name of the colon character."> + Colon + </message> + <message name="IDS_CHROMEOS_ACC_QUOTE" desc="The name of the quotation mark character, abbreviated or shortened if possible."> + Quote + </message> + <message name="IDS_CHROMEOS_ACC_COMMA" desc="The name of the comma character."> + Comma + </message> + <message name="IDS_CHROMEOS_ACC_PERIOD" desc="The name of the period character."> + Period + </message> + <message name="IDS_CHROMEOS_ACC_SPACE" desc="The name of a space character."> + Space + </message> + <message name="IDS_CHROMEOS_ACC_TEXT_UNSELECTED" desc="A phrase to indicate to the user that text has been unselected."> + Unselected + </message> </if> <if expr="pp_ifdef('chromeos') and lang == 'ja'"> <message name="IDS_STATUSBAR_IME_JAPANESE_IME_STATUS_HIRAGANA" desc="In the language menu button, this shows a character set in use is set to Hiragana."> diff --git a/chrome/browser/accessibility_events.h b/chrome/browser/accessibility_events.h index a0ef971..4e765c2 100644 --- a/chrome/browser/accessibility_events.h +++ b/chrome/browser/accessibility_events.h @@ -101,6 +101,10 @@ class AccessibilityRadioButtonInfo : public AccessibilityControlInfo { void SetChecked(bool checked) { checked_ = checked; } + int item_index() const { return item_index_; } + int item_count() const { return item_count_; } + bool checked() const { return checked_; } + private: bool checked_; // The 0-based index of this radio button and number of buttons in the group. @@ -125,6 +129,8 @@ class AccessibilityCheckboxInfo : public AccessibilityControlInfo { void SetChecked(bool checked) { checked_ = checked; } + bool checked() const { return checked_; } + private: bool checked_; }; @@ -151,6 +157,9 @@ class AccessibilityTabInfo : public AccessibilityControlInfo { name_ = tab_name; } + int tab_index() const { return tab_index_; } + int tab_count() const { return tab_count_; } + private: // The 0-based index of this tab and number of tabs in the group. int tab_index_; @@ -181,6 +190,10 @@ class AccessibilityComboBoxInfo : public AccessibilityControlInfo { value_ = value; } + int item_index() const { return item_index_; } + int item_count() const { return item_count_; } + const std::string& value() const { return value_; } + private: std::string value_; // The 0-based index of the current item and the number of total items. @@ -214,6 +227,11 @@ class AccessibilityTextBoxInfo : public AccessibilityControlInfo { selection_end_ = selection_end; } + const std::string& value() const { return value_; } + bool password() const { return password_; } + int selection_start() const { return selection_start_; } + int selection_end() const { return selection_end_; } + private: std::string value_; bool password_; @@ -245,6 +263,10 @@ class AccessibilityListBoxInfo : public AccessibilityControlInfo { value_ = value; } + int item_index() const { return item_index_; } + int item_count() const { return item_count_; } + const std::string& value() const { return value_; } + private: std::string value_; // The 0-based index of the current item and the number of total items. @@ -283,6 +305,10 @@ class AccessibilityMenuItemInfo : public AccessibilityControlInfo { virtual void SerializeToDict(DictionaryValue* dict) const; + int item_index() const { return item_index_; } + int item_count() const { return item_count_; } + bool has_submenu() const { return has_submenu_; } + private: bool has_submenu_; // The 0-based index of the current item and the number of total items. diff --git a/chrome/browser/chromeos/login/wizard_accessibility_handler.cc b/chrome/browser/chromeos/login/wizard_accessibility_handler.cc index 6271f12..e98201e 100644 --- a/chrome/browser/chromeos/login/wizard_accessibility_handler.cc +++ b/chrome/browser/chromeos/login/wizard_accessibility_handler.cc @@ -4,33 +4,113 @@ #include "chrome/browser/chromeos/login/wizard_accessibility_handler.h" +#include <algorithm> + +#include "app/l10n_util.h" +#include "base/i18n/char_iterator.h" +#include "base/logging.h" +#include "base/scoped_ptr.h" +#include "base/string_number_conversions.h" #include "chrome/browser/accessibility_events.h" +#include "chrome/browser/browser_process.h" #include "chrome/browser/chromeos/cros/cros_library.h" #include "chrome/browser/chromeos/cros/speech_synthesis_library.h" +#include "chrome/browser/extensions/extension_accessibility_api.h" +#include "chrome/browser/extensions/extension_accessibility_api_constants.h" +#include "chrome/browser/profile.h" +#include "chrome/browser/profile_manager.h" +#include "chrome/common/notification_registrar.h" #include "chrome/common/notification_service.h" +#include "grit/generated_resources.h" + +namespace keys = extension_accessibility_api_constants; + +namespace { + +static std::string SubstringUTF8(std::string str, int start, int len) { + base::UTF8CharIterator iter(&str); + for (int i = 0; i < start; i++) { + if (!iter.Advance()) + return std::string(); + } + + int byte_start = iter.array_pos(); + for (int i = 0; i < len; i++) { + if (!iter.Advance()) + break; + } + int byte_len = iter.array_pos() - byte_start; + + return str.substr(byte_start, byte_len); +} + +// If the string consists of a single character and that character is +// punctuation that is not normally spoken by TTS, replace the string +// with a description of that character (like "period" for "."). +std::string DescribePunctuation(const std::string& str) { + if (str == "!") { + return l10n_util::GetStringUTF8(IDS_CHROMEOS_ACC_EXCLAMATION_POINT); + } else if (str == "(") { + return l10n_util::GetStringUTF8(IDS_CHROMEOS_ACC_LEFT_PAREN); + } else if (str == ")") { + return l10n_util::GetStringUTF8(IDS_CHROMEOS_ACC_RIGHT_PAREN); + } else if (str == ";") { + return l10n_util::GetStringUTF8(IDS_CHROMEOS_ACC_SEMICOLON); + } else if (str == ":") { + return l10n_util::GetStringUTF8(IDS_CHROMEOS_ACC_COLON); + } else if (str == "\"") { + return l10n_util::GetStringUTF8(IDS_CHROMEOS_ACC_QUOTE); + } else if (str == ",") { + return l10n_util::GetStringUTF8(IDS_CHROMEOS_ACC_COMMA); + } else if (str == ".") { + return l10n_util::GetStringUTF8(IDS_CHROMEOS_ACC_PERIOD); + } else if (str == " ") { + return l10n_util::GetStringUTF8(IDS_CHROMEOS_ACC_SPACE); + } else { + return str; + } +} + +// Append words and separate adding a space if needed. Call +// DescribePunctuation on to_append so that single punctuation +// characters are expanded ('.' -> 'period') but punctuation +// in the middle of a larger phrase are handled by the speech +// engine. +void AppendUtterance(std::string to_append, std::string* str) { + if ((*str).size()) + *str += " "; + + *str += DescribePunctuation(to_append); +} + +// Append a localized string from its message ID, adding a space if needed. +void AppendUtterance(int message_id, std::string* str) { + AppendUtterance(l10n_util::GetStringUTF8(message_id), str); +} + +// Append a phrase of the form "3 of 5", adding a space if needed. +void AppendIndexOfCount(int index, int count, std::string* str) { + string16 index_str = base::IntToString16(index); + string16 count_str = base::IntToString16(count); + AppendUtterance(l10n_util::GetStringFUTF8(IDS_CHROMEOS_ACC_INDEX_OF_COUNT, + index_str, + count_str), str); +} + +} // anonymous namespace + +namespace chromeos { void WizardAccessibilityHandler::Observe( NotificationType type, const NotificationSource& source, const NotificationDetails& details) { - const AccessibilityControlInfo *info = NULL; - info = Details<const AccessibilityControlInfo>(details).ptr(); - switch (type.value) { - case NotificationType::ACCESSIBILITY_CONTROL_FOCUSED: - Speak(info->name().c_str(), false, true); - break; - case NotificationType::ACCESSIBILITY_CONTROL_ACTION: - break; - case NotificationType::ACCESSIBILITY_TEXT_CHANGED: - break; - case NotificationType::ACCESSIBILITY_MENU_OPENED: - break; - case NotificationType::ACCESSIBILITY_MENU_CLOSED: - break; - default: - NOTREACHED() << "Cannot recognize notification type: " << type.value; - break; - } + const AccessibilityControlInfo *control_info = + Details<const AccessibilityControlInfo>(details).ptr(); + std::string description; + EarconType earcon = NO_EARCON; + DescribeAccessibilityEvent(type, control_info, &description, &earcon); + Speak(description.c_str(), false, true); } void WizardAccessibilityHandler::Speak(const char* speak_str, @@ -50,3 +130,318 @@ void WizardAccessibilityHandler::Speak(const char* speak_str, Speak(speak_str); } } + +void WizardAccessibilityHandler::DescribeAccessibilityEvent( + NotificationType event_type, + const AccessibilityControlInfo* control_info, + std::string* out_spoken_description, + EarconType* out_earcon) { + *out_spoken_description = std::string(); + *out_earcon = NO_EARCON; + + switch (event_type.value) { + case NotificationType::ACCESSIBILITY_CONTROL_FOCUSED: + DescribeControl(control_info, false, out_spoken_description, out_earcon); + break; + case NotificationType::ACCESSIBILITY_CONTROL_ACTION: + DescribeControl(control_info, true, out_spoken_description, out_earcon); + break; + case NotificationType::ACCESSIBILITY_TEXT_CHANGED: + DescribeTextChanged(control_info, out_spoken_description, out_earcon); + break; + case NotificationType::ACCESSIBILITY_MENU_OPENED: + *out_earcon = EARCON_OBJECT_OPENED; + break; + case NotificationType::ACCESSIBILITY_MENU_CLOSED: + *out_earcon = EARCON_OBJECT_CLOSED; + break; + default: + NOTREACHED(); + return; + } + + if (control_info->type() == keys::kTypeTextBox) { + const AccessibilityTextBoxInfo* text_box = + static_cast<const AccessibilityTextBoxInfo*>(control_info); + previous_text_value_ = GetTextBoxValue(text_box); + previous_text_selection_start_ = text_box->selection_start(); + previous_text_selection_end_ = text_box->selection_end(); + } +} + +void WizardAccessibilityHandler::DescribeControl( + const AccessibilityControlInfo* control_info, + bool is_action, + std::string* out_spoken_description, + EarconType* out_earcon) { + if (control_info->type() == keys::kTypeButton) { + *out_earcon = EARCON_BUTTON; + AppendUtterance(control_info->name(), out_spoken_description); + AppendUtterance(IDS_CHROMEOS_ACC_BUTTON, out_spoken_description); + } else if (control_info->type() == keys::kTypeCheckbox) { + AppendUtterance(control_info->name(), out_spoken_description); + const AccessibilityCheckboxInfo* checkbox_info = + static_cast<const AccessibilityCheckboxInfo*>(control_info); + if (checkbox_info->checked()) { + *out_earcon = EARCON_CHECK_ON; + AppendUtterance(IDS_CHROMEOS_ACC_CHECKBOX_CHECKED, + out_spoken_description); + } else { + *out_earcon = EARCON_CHECK_OFF; + AppendUtterance(IDS_CHROMEOS_ACC_CHECKBOX_UNCHECKED, + out_spoken_description); + } + } else if (control_info->type() == keys::kTypeComboBox) { + *out_earcon = EARCON_LISTBOX; + const AccessibilityComboBoxInfo* combobox_info = + static_cast<const AccessibilityComboBoxInfo*>(control_info); + AppendUtterance(combobox_info->value(), out_spoken_description); + AppendUtterance(combobox_info->name(), out_spoken_description); + AppendUtterance(IDS_CHROMEOS_ACC_COMBOBOX, out_spoken_description); + AppendIndexOfCount(combobox_info->item_index() + 1, + combobox_info->item_count(), + out_spoken_description); + } else if (control_info->type() == keys::kTypeLink) { + *out_earcon = EARCON_LINK; + AppendUtterance(control_info->name(), out_spoken_description); + AppendUtterance(IDS_CHROMEOS_ACC_LINK, out_spoken_description); + } else if (control_info->type() == keys::kTypeListBox) { + *out_earcon = EARCON_LISTBOX; + const AccessibilityListBoxInfo* listbox_info = + static_cast<const AccessibilityListBoxInfo*>(control_info); + AppendUtterance(listbox_info->value(), out_spoken_description); + AppendUtterance(listbox_info->name(), out_spoken_description); + AppendUtterance(IDS_CHROMEOS_ACC_LISTBOX, out_spoken_description); + AppendIndexOfCount(listbox_info->item_index() + 1, + listbox_info->item_count(), + out_spoken_description); + } else if (control_info->type() == keys::kTypeMenu) { + *out_earcon = EARCON_MENU; + AppendUtterance(control_info->name(), out_spoken_description); + AppendUtterance(IDS_CHROMEOS_ACC_MENU, out_spoken_description); + } else if (control_info->type() == keys::kTypeMenuItem) { + const AccessibilityMenuItemInfo* menu_item_info = + static_cast<const AccessibilityMenuItemInfo*>(control_info); + AppendUtterance(menu_item_info->name(), out_spoken_description); + if (menu_item_info->has_submenu()) + AppendUtterance(IDS_CHROMEOS_ACC_HAS_SUBMENU, out_spoken_description); + AppendIndexOfCount(menu_item_info->item_index() + 1, + menu_item_info->item_count(), + out_spoken_description); + } else if (control_info->type() == keys::kTypeRadioButton) { + AppendUtterance(control_info->name(), out_spoken_description); + const AccessibilityRadioButtonInfo* radio_info = + static_cast<const AccessibilityRadioButtonInfo*>(control_info); + if (radio_info->checked()) { + *out_earcon = EARCON_CHECK_ON; + AppendUtterance(IDS_CHROMEOS_ACC_RADIO_SELECTED, out_spoken_description); + } else { + *out_earcon = EARCON_CHECK_OFF; + AppendUtterance(IDS_CHROMEOS_ACC_RADIO_UNSELECTED, + out_spoken_description); + } + AppendIndexOfCount(radio_info->item_index() + 1, + radio_info->item_count(), + out_spoken_description); + } else if (control_info->type() == keys::kTypeTab) { + *out_earcon = EARCON_TAB; + AppendUtterance(control_info->name(), out_spoken_description); + const AccessibilityTabInfo* tab_info = + static_cast<const AccessibilityTabInfo*>(control_info); + AppendUtterance(IDS_CHROMEOS_ACC_TAB, out_spoken_description); + AppendIndexOfCount(tab_info->tab_index() + 1, + tab_info->tab_count(), + out_spoken_description); + } else if (control_info->type() == keys::kTypeTextBox) { + *out_earcon = EARCON_TEXTBOX; + const AccessibilityTextBoxInfo* textbox_info = + static_cast<const AccessibilityTextBoxInfo*>(control_info); + AppendUtterance(GetTextBoxValue(textbox_info), out_spoken_description); + AppendUtterance(textbox_info->name(), out_spoken_description); + if (textbox_info->password()) { + AppendUtterance(IDS_CHROMEOS_ACC_PASSWORDBOX, out_spoken_description); + } else { + AppendUtterance(IDS_CHROMEOS_ACC_TEXTBOX, out_spoken_description); + } + } else if (control_info->type() == keys::kTypeWindow) { + // No feedback when a window gets focus + } + + if (is_action) + AppendUtterance(IDS_CHROMEOS_ACC_SELECTED, out_spoken_description); +} + +void WizardAccessibilityHandler::DescribeTextChanged( + const AccessibilityControlInfo* control_info, + std::string* out_spoken_description, + EarconType* out_earcon) { + DCHECK_EQ(control_info->type(), keys::kTypeTextBox); + const AccessibilityTextBoxInfo* text_box = + static_cast<const AccessibilityTextBoxInfo*>(control_info); + + std::string old_value = previous_text_value_; + int old_start = previous_text_selection_start_; + int old_end = previous_text_selection_end_; + std::string new_value = GetTextBoxValue(text_box); + int new_start = text_box->selection_start(); + int new_end = text_box->selection_end(); + + if (new_value == old_value) { + DescribeTextSelectionChanged(new_value, + old_start, old_end, + new_start, new_end, + out_spoken_description); + } else { + DescribeTextContentsChanged(old_value, new_value, + out_spoken_description); + } +} + +std::string WizardAccessibilityHandler::GetTextBoxValue( + const AccessibilityTextBoxInfo* textbox_info) { + std::string value = textbox_info->value(); + if (textbox_info->password()) { + base::UTF8CharIterator iter(&value); + std::string obscured; + while (!iter.end()) { + obscured += "*"; + iter.Advance(); + } + return obscured; + } else { + return value; + } +} + +void WizardAccessibilityHandler::DescribeTextSelectionChanged( + const std::string& value, + int old_start, + int old_end, + int new_start, + int new_end, + std::string* out_spoken_description) { + if (new_start == new_end) { + // It's currently a cursor. + if (old_start != old_end) { + // It was previously a selection, so just announce 'unselected'. + AppendUtterance(IDS_CHROMEOS_ACC_TEXT_UNSELECTED, out_spoken_description); + } else if (old_start == new_start + 1 || old_start == new_start - 1) { + // Moved by one character; read it. + AppendUtterance(SubstringUTF8(value, std::min(old_start, new_start), 1), + out_spoken_description); + } else { + // Moved by more than one character. Read all characters crossed. + AppendUtterance(SubstringUTF8(value, + std::min(old_start, new_start), + abs(old_start - new_start)), + out_spoken_description); + } + } else { + // It's currently a selection. + if (old_start == old_end) { + // It was previously a cursor. + AppendUtterance(SubstringUTF8(value, new_start, new_end - new_start), + out_spoken_description); + } else if (old_start == new_start && old_end < new_end) { + // Added to end of selection. + AppendUtterance(SubstringUTF8(value, old_end, new_end - old_end), + out_spoken_description); + } else if (old_start == new_start && old_end > new_end) { + // Removed from end of selection. + AppendUtterance(SubstringUTF8(value, new_end, old_end - new_end), + out_spoken_description); + } else if (old_end == new_end && old_start > new_start) { + // Added to beginning of selection. + AppendUtterance(SubstringUTF8(value, new_start, old_start - new_start), + out_spoken_description); + } else if (old_end == new_end && old_start < new_start) { + // Removed from beginning of selection. + AppendUtterance(SubstringUTF8(value, old_start, new_start - old_start), + out_spoken_description); + } else { + // The selection changed but it wasn't an obvious extension of + // a previous selection. Just read the new selection. + AppendUtterance(SubstringUTF8(value, new_start, new_end - new_start), + out_spoken_description); + } + } +} + +void WizardAccessibilityHandler::DescribeTextContentsChanged( + const std::string& old_value, + const std::string& new_value, + std::string* out_spoken_description) { + int old_array_len = old_value.size(); + int new_array_len = new_value.size(); + + // Get the unicode characters and indices of the start of each + // character's UTF8-encoded representation. + scoped_array<int32> old_chars(new int32[old_array_len]); + scoped_array<int> old_indices(new int[old_array_len + 1]); + base::UTF8CharIterator old_iter(&old_value); + while (!old_iter.end()) { + old_chars[old_iter.char_pos()] = old_iter.get(); + old_indices[old_iter.char_pos()] = old_iter.array_pos(); + old_iter.Advance(); + } + int old_char_len = old_iter.char_pos(); + old_indices[old_char_len] = old_iter.array_pos(); + + scoped_array<int32> new_chars(new int32[new_array_len]); + scoped_array<int> new_indices(new int[new_array_len + 1]); + base::UTF8CharIterator new_iter(&new_value); + while (!new_iter.end()) { + new_chars[new_iter.char_pos()] = new_iter.get(); + new_indices[new_iter.char_pos()] = new_iter.array_pos(); + new_iter.Advance(); + } + int new_char_len = new_iter.char_pos(); + new_indices[new_char_len] = new_iter.array_pos(); + + // Find the common prefix of the two strings. + int prefix_char_len = 0; + while (prefix_char_len < old_char_len && + prefix_char_len < new_char_len && + old_chars[prefix_char_len] == new_chars[prefix_char_len]) { + prefix_char_len++; + } + + // Find the common suffix of the two stirngs. + int suffix_char_len = 0; + while (suffix_char_len < old_char_len - prefix_char_len && + suffix_char_len < new_char_len - prefix_char_len && + (old_chars[old_char_len - suffix_char_len - 1] == + new_chars[new_char_len - suffix_char_len - 1])) { + suffix_char_len++; + } + + int old_suffix_char_start = old_char_len - suffix_char_len; + int new_suffix_char_start = new_char_len - suffix_char_len; + + // Find the substring that was deleted (if any) to get the new string + // from the old - it's the part in the middle of the old string if you + // remove the common prefix and suffix. + std::string deleted = old_value.substr( + old_indices[prefix_char_len], + old_indices[old_suffix_char_start] - old_indices[prefix_char_len]); + + // Find the substring that was inserted (if any) to get the new string + // from the old - it's the part in the middle of the new string if you + // remove the common prefix and suffix. + std::string inserted = new_value.substr( + new_indices[prefix_char_len], + new_indices[new_suffix_char_start] - new_indices[prefix_char_len]); + + if (inserted.size() > 0 && deleted.size() > 0) { + // Replace one substring with another, speak inserted text. + AppendUtterance(inserted, out_spoken_description); + } else if (inserted.size() > 0) { + // Speak inserted text. + AppendUtterance(inserted, out_spoken_description); + } else if (deleted.size() > 0) { + // Speak deleted text. + AppendUtterance(deleted, out_spoken_description); + } +} + +} // namespace chromeos diff --git a/chrome/browser/chromeos/login/wizard_accessibility_handler.h b/chrome/browser/chromeos/login/wizard_accessibility_handler.h index 68987b4..5fda4dd 100644 --- a/chrome/browser/chromeos/login/wizard_accessibility_handler.h +++ b/chrome/browser/chromeos/login/wizard_accessibility_handler.h @@ -6,23 +6,94 @@ #define CHROME_BROWSER_CHROMEOS_LOGIN_WIZARD_ACCESSIBILITY_HANDLER_H_ #pragma once +#include <string> + +#include "base/gtest_prod_util.h" #include "chrome/common/notification_observer.h" #include "chrome/common/notification_source.h" #include "chrome/common/notification_type.h" +class AccessibilityControlInfo; +class AccessibilityTextBoxInfo; + +namespace chromeos { + +enum EarconType { + NO_EARCON, + EARCON_BUTTON, + EARCON_CHECK_OFF, + EARCON_CHECK_ON, + EARCON_ELLIPSES, + EARCON_LINK, + EARCON_LISTBOX, + EARCON_MENU, + EARCON_OBJECT_OPENED, + EARCON_OBJECT_CLOSED, + EARCON_TAB, + EARCON_TEXTBOX, +}; + // Class that handles the accessibility notifications and generates // appropriate spoken/audio feedback. class WizardAccessibilityHandler : public NotificationObserver { public: + WizardAccessibilityHandler() { } + // Speaks the specified string. void Speak(const char* speak_str, bool queue, bool interruptible); private: - // Override from NotificationObserver. virtual void Observe(NotificationType type, const NotificationSource& source, const NotificationDetails& details); + + // Get text to speak and an earcon identifier (which may be NONE) for any + // accessibility event. + void DescribeAccessibilityEvent(NotificationType event_type, + const AccessibilityControlInfo* control_info, + std::string* out_spoken_description, + EarconType* out_earcon); + + // Get text to speak and an optional earcon identifier, specifically for + // a focus or select accessibility event on a control. + void DescribeControl(const AccessibilityControlInfo* control_info, + bool is_action, + std::string* out_spoken_description, + EarconType* out_earcon); + + // Get text to speak when a text control has changed in some way, either + // the contents or selection/cursor. + void DescribeTextChanged(const AccessibilityControlInfo* control_info, + std::string* out_spoken_description, + EarconType* out_earcon); + + // Get the text from an AccessibilityTextBoxInfo, obscuring the + // text if it's a password field. + std::string GetTextBoxValue(const AccessibilityTextBoxInfo* textbox_info); + + // Get text to speak when only the selection/cursor has changed. + void DescribeTextSelectionChanged(const std::string& value, + int old_start, int old_end, + int new_start, int new_end, + std::string* out_spoken_description); + + // Get text to speak when the contents of a text control has changed. + void DescribeTextContentsChanged(const std::string& old_value, + const std::string& new_value, + std::string* out_spoken_description); + + int previous_text_selection_start_; + int previous_text_selection_end_; + std::string previous_text_value_; + + friend class WizardAccessibilityHandlerTest; + FRIEND_TEST_ALL_PREFIXES(WizardAccessibilityHandlerTest, TestFocusEvents); + FRIEND_TEST_ALL_PREFIXES(WizardAccessibilityHandlerTest, TestTextEvents); + + DISALLOW_COPY_AND_ASSIGN(WizardAccessibilityHandler); }; +} // namespace chromeos + #endif // CHROME_BROWSER_CHROMEOS_LOGIN_WIZARD_ACCESSIBILITY_HANDLER_H_ diff --git a/chrome/browser/chromeos/login/wizard_accessibility_handler_unittest.cc b/chrome/browser/chromeos/login/wizard_accessibility_handler_unittest.cc new file mode 100644 index 0000000..b1afe99 --- /dev/null +++ b/chrome/browser/chromeos/login/wizard_accessibility_handler_unittest.cc @@ -0,0 +1,164 @@ +// 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/accessibility_events.h" +#include "chrome/browser/chromeos/login/wizard_accessibility_handler.h" +#include "testing/gtest/include/gtest/gtest.h" + +using chromeos::EarconType; +using chromeos::WizardAccessibilityHandler; + +namespace chromeos { + +class WizardAccessibilityHandlerTest : public testing::Test { + protected: + void ChangeText(WizardAccessibilityHandler* handler, + AccessibilityTextBoxInfo* textbox_info, + const std::string& value, + int selection_start, + int selection_end, + std::string* description) { + textbox_info->SetValue(value, selection_start, selection_end); + EarconType earcon = chromeos::NO_EARCON; + handler->DescribeAccessibilityEvent( + NotificationType::ACCESSIBILITY_TEXT_CHANGED, + textbox_info, + description, + &earcon); + } +}; + +TEST_F(WizardAccessibilityHandlerTest, TestFocusEvents) { + WizardAccessibilityHandler handler; + + std::string description; + EarconType earcon; + + // No need to test every possible control, but test several types + // to exercise different types of string concatenation. + + // NOTE: unittests are forced to run under the en-US locale, so it's + // safe to do these string comparisons without using l10n_util. + + // Test a simple control. + std::string button_name = "Save"; + AccessibilityButtonInfo button_info(NULL, button_name); + handler.DescribeAccessibilityEvent( + NotificationType::ACCESSIBILITY_CONTROL_FOCUSED, + &button_info, + &description, + &earcon); + EXPECT_EQ(chromeos::EARCON_BUTTON, earcon); + EXPECT_EQ("Save Button", description); + + // Test a control with multiple states. + std::string checkbox_name = "Accessibility"; + AccessibilityCheckboxInfo checkbox_info(NULL, checkbox_name, false); + handler.DescribeAccessibilityEvent( + NotificationType::ACCESSIBILITY_CONTROL_FOCUSED, + &checkbox_info, + &description, + &earcon); + EXPECT_EQ(chromeos::EARCON_CHECK_OFF, earcon); + EXPECT_EQ("Accessibility Unchecked check box", description); + checkbox_info.SetChecked(true); + handler.DescribeAccessibilityEvent( + NotificationType::ACCESSIBILITY_CONTROL_FOCUSED, + &checkbox_info, + &description, + &earcon); + EXPECT_EQ(chromeos::EARCON_CHECK_ON, earcon); + EXPECT_EQ("Accessibility Checked check box", description); + + // Test a control with a value and index. + std::string combobox_name = "Language"; + std::string combobox_value = "English"; + AccessibilityComboBoxInfo combobox_info( + NULL, combobox_name, combobox_value, 12, 35); + handler.DescribeAccessibilityEvent( + NotificationType::ACCESSIBILITY_CONTROL_FOCUSED, + &combobox_info, + &description, + &earcon); + EXPECT_EQ(chromeos::EARCON_LISTBOX, earcon); + EXPECT_EQ("English Language Combo box 13 of 35", description); +} + +TEST_F(WizardAccessibilityHandlerTest, TestTextEvents) { + WizardAccessibilityHandler handler; + + std::string description; + EarconType earcon; + + AccessibilityTextBoxInfo textbox_info(NULL, "", false); + handler.DescribeAccessibilityEvent( + NotificationType::ACCESSIBILITY_CONTROL_FOCUSED, + &textbox_info, + &description, + &earcon); + EXPECT_EQ("Text box", description); + EXPECT_EQ(chromeos::EARCON_TEXTBOX, earcon); + + // Type "hello world.", one character at a time. + ChangeText(&handler, &textbox_info, "h", 1, 1, &description); + EXPECT_EQ("h", description); + ChangeText(&handler, &textbox_info, "he", 2, 2, &description); + EXPECT_EQ("e", description); + ChangeText(&handler, &textbox_info, "hel", 3, 3, &description); + EXPECT_EQ("l", description); + ChangeText(&handler, &textbox_info, "hell", 4, 4, &description); + EXPECT_EQ("l", description); + ChangeText(&handler, &textbox_info, "hello", 5, 5, &description); + EXPECT_EQ("o", description); + ChangeText(&handler, &textbox_info, "hello ", 6, 6, &description); + EXPECT_EQ("Space", description); + ChangeText(&handler, &textbox_info, "hello w", 7, 7, &description); + EXPECT_EQ("w", description); + ChangeText(&handler, &textbox_info, "hello wo", 8, 8, &description); + EXPECT_EQ("o", description); + ChangeText(&handler, &textbox_info, "hello wor", 9, 9, &description); + EXPECT_EQ("r", description); + ChangeText(&handler, &textbox_info, "hello worl", 10, 10, &description); + EXPECT_EQ("l", description); + ChangeText(&handler, &textbox_info, "hello world", 11, 11, &description); + EXPECT_EQ("d", description); + ChangeText(&handler, &textbox_info, "hello world.", 12, 12, &description); + EXPECT_EQ("Period", description); + + // Move by characters and by words. + ChangeText(&handler, &textbox_info, "hello world.", 11, 11, &description); + EXPECT_EQ("Period", description); + ChangeText(&handler, &textbox_info, "hello world.", 6, 6, &description); + EXPECT_EQ("world", description); + ChangeText(&handler, &textbox_info, "hello world.", 0, 0, &description); + EXPECT_EQ("hello ", description); + ChangeText(&handler, &textbox_info, "hello world.", 1, 1, &description); + EXPECT_EQ("h", description); + ChangeText(&handler, &textbox_info, "hello world.", 5, 5, &description); + EXPECT_EQ("ello", description); + + // Delete characters and words. + ChangeText(&handler, &textbox_info, "hell world.", 4, 4, &description); + EXPECT_EQ("o", description); + ChangeText(&handler, &textbox_info, "hel world.", 3, 3, &description); + EXPECT_EQ("l", description); + ChangeText(&handler, &textbox_info, " world.", 0, 0, &description); + EXPECT_EQ("hel", description); + + // Select characters and words. + ChangeText(&handler, &textbox_info, " world.", 0, 1, &description); + EXPECT_EQ("Space", description); + ChangeText(&handler, &textbox_info, " world.", 0, 4, &description); + EXPECT_EQ("wor", description); + ChangeText(&handler, &textbox_info, " world.", 1, 4, &description); + EXPECT_EQ("Space", description); + ChangeText(&handler, &textbox_info, " world.", 4, 4, &description); + EXPECT_EQ("Unselected", description); + + // If the string suddenly changes, it should just speak the new value. + ChangeText(&handler, &textbox_info, "Potato", 0, 0, &description); + EXPECT_EQ("Potato", description); +} + +} diff --git a/chrome/browser/chromeos/login/wizard_accessibility_helper.cc b/chrome/browser/chromeos/login/wizard_accessibility_helper.cc index eea0bc2..db0f097 100644 --- a/chrome/browser/chromeos/login/wizard_accessibility_helper.cc +++ b/chrome/browser/chromeos/login/wizard_accessibility_helper.cc @@ -17,6 +17,8 @@ #include "views/accelerator.h" #include "views/view.h" +namespace chromeos { + scoped_ptr<views::Accelerator> WizardAccessibilityHelper::accelerator_; // static @@ -114,3 +116,5 @@ void WizardAccessibilityHelper::AddViewToBuffer(views::View* view_tree) { if (!view_exists) views_buffer_[view_tree] = false; } + +} // namespace chromeos diff --git a/chrome/browser/chromeos/login/wizard_accessibility_helper.h b/chrome/browser/chromeos/login/wizard_accessibility_helper.h index 9344a47..27c9a9b 100644 --- a/chrome/browser/chromeos/login/wizard_accessibility_helper.h +++ b/chrome/browser/chromeos/login/wizard_accessibility_helper.h @@ -22,6 +22,8 @@ class Accelerator; class View; } +namespace chromeos { + // Class that provides convenience methods to enable accessibility for a // specified View. class WizardAccessibilityHelper { @@ -74,4 +76,6 @@ class WizardAccessibilityHelper { DISALLOW_COPY_AND_ASSIGN(WizardAccessibilityHelper); }; +} // namespace chromeos + #endif // CHROME_BROWSER_CHROMEOS_LOGIN_WIZARD_ACCESSIBILITY_HELPER_H_ diff --git a/chrome/browser/chromeos/login/wizard_controller.cc b/chrome/browser/chromeos/login/wizard_controller.cc index fa0f938..2a8afdd 100644 --- a/chrome/browser/chromeos/login/wizard_controller.cc +++ b/chrome/browser/chromeos/login/wizard_controller.cc @@ -79,7 +79,7 @@ class ContentView : public views::View { public: ContentView() : accel_enable_accessibility_( - WizardAccessibilityHelper::GetAccelerator()) { + chromeos::WizardAccessibilityHelper::GetAccelerator()) { AddAccelerator(accel_enable_accessibility_); #if !defined(OFFICIAL_BUILD) accel_account_screen_ = views::Accelerator(app::VKEY_A, @@ -119,7 +119,7 @@ class ContentView : public views::View { return false; if (accel == accel_enable_accessibility_) { - WizardAccessibilityHelper::GetInstance()->EnableAccessibility( + chromeos::WizardAccessibilityHelper::GetInstance()->EnableAccessibility( controller->contents()); #if !defined(OFFICIAL_BUILD) } else if (accel == accel_account_screen_) { diff --git a/chrome/browser/extensions/extension_accessibility_api.cc b/chrome/browser/extensions/extension_accessibility_api.cc index bf310f2..83a6194 100644 --- a/chrome/browser/extensions/extension_accessibility_api.cc +++ b/chrome/browser/extensions/extension_accessibility_api.cc @@ -64,6 +64,12 @@ void ExtensionAccessibilityEventRouter::ObserveProfile(Profile* profile) { registrar_.Add(this, NotificationType::ACCESSIBILITY_TEXT_CHANGED, NotificationService::AllSources()); + registrar_.Add(this, + NotificationType::ACCESSIBILITY_MENU_OPENED, + NotificationService::AllSources()); + registrar_.Add(this, + NotificationType::ACCESSIBILITY_MENU_CLOSED, + NotificationService::AllSources()); } } diff --git a/chrome/browser/views/accessible_view_helper.cc b/chrome/browser/views/accessible_view_helper.cc index 63f381b..aa86ef9 100644 --- a/chrome/browser/views/accessible_view_helper.cc +++ b/chrome/browser/views/accessible_view_helper.cc @@ -9,6 +9,7 @@ #include "chrome/browser/profile.h" #include "chrome/browser/views/accessibility_event_router_views.h" #include "chrome/common/notification_service.h" +#include "views/controls/native/native_view_host.h" #include "views/widget/widget.h" #include "views/view.h" @@ -59,6 +60,12 @@ void AccessibleViewHelper::IgnoreView(views::View* view) { accessibility_event_router_->IgnoreView(view); managed_views_.push_back(view); + + #if defined(OS_LINUX) + gfx::NativeView native_view = GetNativeView(view); + if (native_view) + widget_helper_->IgnoreWidget(native_view); + #endif } void AccessibleViewHelper::SetViewName(views::View* view, std::string name) { @@ -67,13 +74,28 @@ void AccessibleViewHelper::SetViewName(views::View* view, std::string name) { accessibility_event_router_->SetViewName(view, name); managed_views_.push_back(view); + + #if defined(OS_LINUX) + gfx::NativeView native_view = GetNativeView(view); + if (native_view) + widget_helper_->SetWidgetName(native_view, name); + #endif } void AccessibleViewHelper::SetViewName(views::View* view, int string_id) { - if (!view_tree_) - return; + const std::string name = l10n_util::GetStringUTF8(string_id); + SetViewName(view, name); +} - std::string name = l10n_util::GetStringUTF8(string_id); - accessibility_event_router_->SetViewName(view, name); - managed_views_.push_back(view); +gfx::NativeView AccessibleViewHelper::GetNativeView(views::View* view) const { + if (view->GetClassName() == views::NativeViewHost::kViewClassName) + return static_cast<views::NativeViewHost*>(view)->native_view(); + + for (int i = 0; i < view->GetChildViewCount(); i++) { + gfx::NativeView native_view = GetNativeView(view->GetChildViewAt(i)); + if (native_view) + return native_view; + } + + return NULL; } diff --git a/chrome/browser/views/accessible_view_helper.h b/chrome/browser/views/accessible_view_helper.h index 2f6f434..e3a0f93 100644 --- a/chrome/browser/views/accessible_view_helper.h +++ b/chrome/browser/views/accessible_view_helper.h @@ -13,6 +13,7 @@ #include "base/scoped_ptr.h" #include "base/singleton.h" #include "chrome/browser/accessibility_events.h" +#include "gfx/native_widget_types.h" #if defined(OS_LINUX) #include "chrome/browser/gtk/accessible_widget_helper_gtk.h" @@ -66,6 +67,9 @@ class AccessibleViewHelper { void SetViewName(views::View* view, int string_id); private: + // Returns a native view if the given view has a native view in it. + gfx::NativeView GetNativeView(views::View* view) const; + AccessibilityEventRouterViews* accessibility_event_router_; Profile* profile_; views::View* view_tree_; diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi index f020871..622cb85a 100644 --- a/chrome/chrome_tests.gypi +++ b/chrome/chrome_tests.gypi @@ -877,6 +877,7 @@ 'browser/chromeos/login/owner_key_utils_unittest.cc', 'browser/chromeos/login/owner_manager_unittest.cc', 'browser/chromeos/login/ownership_service_unittest.cc', + 'browser/chromeos/login/wizard_accessibility_handler_unittest.cc', 'browser/chromeos/login/signed_settings_unittest.cc', 'browser/chromeos/login/signed_settings_helper_unittest.cc', 'browser/chromeos/notifications/desktop_notifications_unittest.cc', |