Chromium Code Reviews| Index: chrome/browser/chromeos/input_method/xkeyboard.cc |
| diff --git a/chrome/browser/chromeos/input_method/xkeyboard.cc b/chrome/browser/chromeos/input_method/xkeyboard.cc |
| index 2f1d3db9d22635ff6a0786d7a733be831fc3246e..4a08f5343dc9b669cf13d61187a85492828ceff7 100644 |
| --- a/chrome/browser/chromeos/input_method/xkeyboard.cc |
| +++ b/chrome/browser/chromeos/input_method/xkeyboard.cc |
| @@ -4,14 +4,12 @@ |
| #include "chrome/browser/chromeos/input_method/xkeyboard.h" |
| +#include <cstdlib> |
| +#include <cstring> |
| #include <queue> |
| #include <set> |
| -#include <string> |
| #include <utility> |
| -#include <stdlib.h> |
| -#include <string.h> |
| - |
| #include "base/logging.h" |
| #include "base/memory/scoped_ptr.h" |
| #include "base/process_util.h" |
| @@ -101,9 +99,79 @@ const char* kCapsLockRemapped[] = { |
| // A string for obtaining a mask value for Num Lock. |
| const char kNumLockVirtualModifierString[] = "NumLock"; |
| -} // namespace |
| +class XKeyboardImpl : public XKeyboard { |
| + public: |
| + explicit XKeyboardImpl(const InputMethodUtil& util); |
| + virtual ~XKeyboardImpl() {} |
| + |
|
mazda
2012/01/20 02:31:43
// Overriden from XKeyboard:
Yusuke Sato
2012/01/20 03:33:48
Done.
|
| + virtual bool SetCurrentKeyboardLayoutByName( |
| + const std::string& layout_name) OVERRIDE; |
| + virtual bool RemapModifierKeys(const ModifierMap& modifier_map) OVERRIDE; |
| + virtual bool ReapplyCurrentKeyboardLayout() OVERRIDE; |
| + virtual void ReapplyCurrentModifierLockStatus() OVERRIDE; |
| + virtual void SetLockedModifiers( |
| + ModifierLockStatus new_caps_lock_status, |
| + ModifierLockStatus new_num_lock_status) OVERRIDE; |
| + virtual void SetNumLockEnabled(bool enable_num_lock) OVERRIDE; |
| + virtual void SetCapsLockEnabled(bool enable_caps_lock) OVERRIDE; |
| + virtual bool NumLockIsEnabled() OVERRIDE; |
| + virtual bool CapsLockIsEnabled() OVERRIDE; |
| + virtual unsigned int GetNumLockMask() OVERRIDE; |
| + virtual void GetLockedModifiers(bool* out_caps_lock_enabled, |
| + bool* out_num_lock_enabled) OVERRIDE; |
| + virtual std::string CreateFullXkbLayoutName( |
| + const std::string& layout_name, |
| + const ModifierMap& modifire_map) OVERRIDE; |
| + |
| + private: |
| + // This function is used by SetLayout() and RemapModifierKeys(). Calls |
| + // setxkbmap command if needed, and updates the last_full_layout_name_ cache. |
| + bool SetLayoutInternal(const std::string& layout_name, |
| + const ModifierMap& modifier_map, |
| + bool force); |
| + |
| + // Executes 'setxkbmap -layout ...' command asynchronously using a layout name |
| + // in the |execute_queue_|. Do nothing if the queue is empty. |
| + // TODO(yusukes): Use libxkbfile.so instead of the command (crosbug.com/13105) |
| + void MaybeExecuteSetLayoutCommand(); |
| + |
| + // Returns true if the XKB layout uses the right Alt key for special purposes |
| + // like AltGr. |
| + bool KeepRightAlt(const std::string& xkb_layout_name) const; |
| + |
| + // Returns true if the XKB layout uses the CapsLock key for special purposes. |
| + // For example, since US Colemak layout uses the key as back space, |
| + // KeepCapsLock("us(colemak)") would return true. |
| + bool KeepCapsLock(const std::string& xkb_layout_name) const; |
| + |
| + // Converts |key| to a modifier key name which is used in |
| + // /usr/share/X11/xkb/symbols/chromeos. |
| + static std::string ModifierKeyToString(ModifierKey key); |
| + |
| + // Called when execve'd setxkbmap process exits. |
| + static void OnSetLayoutFinish(pid_t pid, int status, XKeyboardImpl* self); |
| + |
| + const bool is_running_on_chrome_os_; |
| + unsigned int num_lock_mask_; |
| + |
| + // The current Num Lock and Caps Lock status. If true, enabled. |
| + bool current_num_lock_status_; |
| + bool current_caps_lock_status_; |
| + // The XKB layout name which we set last time like "us" and "us(dvorak)". |
| + std::string current_layout_name_; |
| + // The mapping of modifier keys we set last time. |
| + ModifierMap current_modifier_map_; |
| + |
| + // A queue for executing setxkbmap one by one. |
| + std::queue<std::string> execute_queue_; |
| + |
| + std::set<std::string> keep_right_alt_xkb_layout_names_; |
| + std::set<std::string> caps_lock_remapped_xkb_layout_names_; |
| + |
| + DISALLOW_COPY_AND_ASSIGN(XKeyboardImpl); |
| +}; |
| -XKeyboard::XKeyboard(const InputMethodUtil& util) |
| +XKeyboardImpl::XKeyboardImpl(const InputMethodUtil& util) |
| : is_running_on_chrome_os_( |
| system::runtime_environment::IsRunningOnChromeOS()) { |
| num_lock_mask_ = GetNumLockMask(); |
| @@ -116,8 +184,7 @@ XKeyboard::XKeyboard(const InputMethodUtil& util) |
| CHECK(!is_running_on_chrome_os_ || (num_lock_mask_ == Mod2Mask)); |
| #endif |
| - GetLockedModifiers( |
| - num_lock_mask_, ¤t_caps_lock_status_, ¤t_num_lock_status_); |
| + GetLockedModifiers(¤t_caps_lock_status_, ¤t_num_lock_status_); |
| for (size_t i = 0; i < arraysize(kCustomizableKeys); ++i) { |
| ModifierKey key = kCustomizableKeys[i]; |
| @@ -144,45 +211,7 @@ XKeyboard::XKeyboard(const InputMethodUtil& util) |
| } |
| } |
| -XKeyboard::~XKeyboard() { |
| -} |
| - |
| -// static |
| -unsigned int XKeyboard::GetNumLockMask() { |
| - static const unsigned int kBadMask = 0; |
| - |
| - unsigned int real_mask = kBadMask; |
| - XkbDescPtr xkb_desc = |
| - XkbGetKeyboard(ui::GetXDisplay(), XkbAllComponentsMask, XkbUseCoreKbd); |
| - if (!xkb_desc) { |
| - return kBadMask; |
| - } |
| - |
| - if (xkb_desc->dpy && xkb_desc->names && xkb_desc->names->vmods) { |
| - const std::string string_to_find(kNumLockVirtualModifierString); |
| - for (size_t i = 0; i < XkbNumVirtualMods; ++i) { |
| - const unsigned int virtual_mod_mask = 1U << i; |
| - char* virtual_mod_str = |
| - XGetAtomName(xkb_desc->dpy, xkb_desc->names->vmods[i]); |
| - if (!virtual_mod_str) { |
| - continue; |
| - } |
| - if (string_to_find == virtual_mod_str) { |
| - if (!XkbVirtualModsToReal(xkb_desc, virtual_mod_mask, &real_mask)) { |
| - LOG(ERROR) << "XkbVirtualModsToReal failed"; |
| - real_mask = kBadMask; // reset the return value, just in case. |
| - } |
| - XFree(virtual_mod_str); |
| - break; |
| - } |
| - XFree(virtual_mod_str); |
| - } |
| - } |
| - XkbFreeKeyboard(xkb_desc, 0, True /* free all components */); |
| - return real_mask; |
| -} |
| - |
| -bool XKeyboard::SetLayoutInternal(const std::string& layout_name, |
| +bool XKeyboardImpl::SetLayoutInternal(const std::string& layout_name, |
| const ModifierMap& modifier_map, |
| bool force) { |
| if (!is_running_on_chrome_os_) { |
| @@ -229,7 +258,7 @@ bool XKeyboard::SetLayoutInternal(const std::string& layout_name, |
| // Executes 'setxkbmap -layout ...' command asynchronously using a layout name |
| // in the |execute_queue_|. Do nothing if the queue is empty. |
| // TODO(yusukes): Use libxkbfile.so instead of the command (crosbug.com/13105) |
| -void XKeyboard::MaybeExecuteSetLayoutCommand() { |
| +void XKeyboardImpl::MaybeExecuteSetLayoutCommand() { |
| if (execute_queue_.empty()) { |
| return; |
| } |
| @@ -258,20 +287,82 @@ void XKeyboard::MaybeExecuteSetLayoutCommand() { |
| VLOG(1) << "ExecuteSetLayoutCommand: " << layout_to_set << ": pid=" << pid; |
| } |
| -// static |
| -void XKeyboard::OnSetLayoutFinish(pid_t pid, int status, XKeyboard* self) { |
| - CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| - VLOG(1) << "OnSetLayoutFinish: pid=" << pid; |
| - if (self->execute_queue_.empty()) { |
| - LOG(ERROR) << "OnSetLayoutFinish: execute_queue_ is empty. " |
| - << "base::LaunchProcess failed? pid=" << pid; |
| +bool XKeyboardImpl::NumLockIsEnabled() { |
| + bool num_lock_enabled = false; |
| + GetLockedModifiers(NULL /* Caps Lock */, &num_lock_enabled); |
| + return num_lock_enabled; |
| +} |
| + |
| +bool XKeyboardImpl::CapsLockIsEnabled() { |
| + bool caps_lock_enabled = false; |
| + GetLockedModifiers(&caps_lock_enabled, NULL /* Num Lock */); |
| + return caps_lock_enabled; |
| +} |
| + |
| +unsigned int XKeyboardImpl::GetNumLockMask() { |
| + static const unsigned int kBadMask = 0; |
| + |
| + unsigned int real_mask = kBadMask; |
| + XkbDescPtr xkb_desc = |
| + XkbGetKeyboard(ui::GetXDisplay(), XkbAllComponentsMask, XkbUseCoreKbd); |
| + if (!xkb_desc) { |
| + return kBadMask; |
| + } |
| + |
| + if (xkb_desc->dpy && xkb_desc->names && xkb_desc->names->vmods) { |
| + const std::string string_to_find(kNumLockVirtualModifierString); |
| + for (size_t i = 0; i < XkbNumVirtualMods; ++i) { |
| + const unsigned int virtual_mod_mask = 1U << i; |
| + char* virtual_mod_str = |
| + XGetAtomName(xkb_desc->dpy, xkb_desc->names->vmods[i]); |
| + if (!virtual_mod_str) { |
| + continue; |
| + } |
| + if (string_to_find == virtual_mod_str) { |
| + if (!XkbVirtualModsToReal(xkb_desc, virtual_mod_mask, &real_mask)) { |
| + LOG(ERROR) << "XkbVirtualModsToReal failed"; |
| + real_mask = kBadMask; // reset the return value, just in case. |
| + } |
| + XFree(virtual_mod_str); |
| + break; |
| + } |
| + XFree(virtual_mod_str); |
| + } |
| + } |
| + XkbFreeKeyboard(xkb_desc, 0, True /* free all components */); |
| + return real_mask; |
| +} |
| + |
| +void XKeyboardImpl::GetLockedModifiers(bool* out_caps_lock_enabled, |
| + bool* out_num_lock_enabled) { |
| + // For now, don't call CHECK() here to make |
| + // TabRestoreServiceTest.DontRestorePrintPreviewTab test happy. |
| + // TODO(yusukes): Fix the test, then fix the if(!BrowserThread...) line below. |
| + // CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| + |
| + if (!BrowserThread::CurrentlyOn(BrowserThread::UI) || |
| + (out_num_lock_enabled && !num_lock_mask_)) { |
| + LOG(ERROR) << "Cannot get locked modifiers."; |
| + if (out_caps_lock_enabled) { |
| + *out_caps_lock_enabled = false; |
| + } |
| + if (out_num_lock_enabled) { |
| + *out_num_lock_enabled = false; |
| + } |
| return; |
| } |
| - self->execute_queue_.pop(); |
| - self->MaybeExecuteSetLayoutCommand(); |
| + |
| + XkbStateRec status; |
| + XkbGetState(ui::GetXDisplay(), XkbUseCoreKbd, &status); |
| + if (out_caps_lock_enabled) { |
| + *out_caps_lock_enabled = status.locked_mods & LockMask; |
| + } |
| + if (out_num_lock_enabled) { |
| + *out_num_lock_enabled = status.locked_mods & num_lock_mask_; |
| + } |
| } |
| -std::string XKeyboard::CreateFullXkbLayoutName( |
| +std::string XKeyboardImpl::CreateFullXkbLayoutName( |
| const std::string& layout_name, const ModifierMap& modifier_map) { |
| static const char kValidLayoutNameCharacters[] = |
| "abcdefghijklmnopqrstuvwxyz0123456789()-_"; |
| @@ -346,49 +437,8 @@ std::string XKeyboard::CreateFullXkbLayoutName( |
| return full_xkb_layout_name; |
| } |
| -// static |
| -bool XKeyboard::SetAutoRepeatEnabled(bool enabled) { |
| - CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| - if (enabled) { |
| - XAutoRepeatOn(ui::GetXDisplay()); |
| - } else { |
| - XAutoRepeatOff(ui::GetXDisplay()); |
| - } |
| - DLOG(INFO) << "Set auto-repeat mode to: " << (enabled ? "on" : "off"); |
| - return true; |
| -} |
| - |
| -// static |
| -bool XKeyboard::SetAutoRepeatRate(const AutoRepeatRate& rate) { |
| - CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| - DLOG(INFO) << "Set auto-repeat rate to: " |
| - << rate.initial_delay_in_ms << " ms delay, " |
| - << rate.repeat_interval_in_ms << " ms interval"; |
| - if (XkbSetAutoRepeatRate(ui::GetXDisplay(), XkbUseCoreKbd, |
| - rate.initial_delay_in_ms, |
| - rate.repeat_interval_in_ms) != True) { |
| - LOG(ERROR) << "Failed to set auto-repeat rate"; |
| - return false; |
| - } |
| - return true; |
| -} |
| - |
| -// static |
| -bool XKeyboard::GetAutoRepeatEnabled() { |
| - XKeyboardState state = {}; |
| - XGetKeyboardControl(ui::GetXDisplay(), &state); |
| - return state.global_auto_repeat != AutoRepeatModeOff; |
| -} |
| - |
| -// static |
| -bool XKeyboard::GetAutoRepeatRate(AutoRepeatRate* out_rate) { |
| - return XkbGetAutoRepeatRate(ui::GetXDisplay(), XkbUseCoreKbd, |
| - &(out_rate->initial_delay_in_ms), |
| - &(out_rate->repeat_interval_in_ms)) == True; |
| -} |
| - |
| -void XKeyboard::SetLockedModifiers(ModifierLockStatus new_caps_lock_status, |
| - ModifierLockStatus new_num_lock_status) { |
| +void XKeyboardImpl::SetLockedModifiers(ModifierLockStatus new_caps_lock_status, |
| + ModifierLockStatus new_num_lock_status) { |
| CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| if (!num_lock_mask_) { |
| LOG(ERROR) << "Cannot set locked modifiers. Num Lock mask unknown."; |
| @@ -413,73 +463,18 @@ void XKeyboard::SetLockedModifiers(ModifierLockStatus new_caps_lock_status, |
| } |
| } |
| -void XKeyboard::SetNumLockEnabled(bool enable_num_lock) { |
| +void XKeyboardImpl::SetNumLockEnabled(bool enable_num_lock) { |
| SetLockedModifiers( |
| kDontChange, enable_num_lock ? kEnableLock : kDisableLock); |
| } |
| -void XKeyboard::SetCapsLockEnabled(bool enable_caps_lock) { |
| +void XKeyboardImpl::SetCapsLockEnabled(bool enable_caps_lock) { |
| SetLockedModifiers( |
| enable_caps_lock ? kEnableLock : kDisableLock, kDontChange); |
| } |
| -// static |
| -void XKeyboard::GetLockedModifiers(unsigned int num_lock_mask, |
| - bool* out_caps_lock_enabled, |
| - bool* out_num_lock_enabled) { |
| - // For now, don't call CHECK() here to make |
| - // TabRestoreServiceTest.DontRestorePrintPreviewTab test happy. |
| - // TODO(yusukes): Fix the test, then fix the if(!BrowserThread...) line below. |
| - // CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| - |
| - if (!BrowserThread::CurrentlyOn(BrowserThread::UI) || |
| - (out_num_lock_enabled && !num_lock_mask)) { |
| - LOG(ERROR) << "Cannot get locked modifiers."; |
| - if (out_caps_lock_enabled) { |
| - *out_caps_lock_enabled = false; |
| - } |
| - if (out_num_lock_enabled) { |
| - *out_num_lock_enabled = false; |
| - } |
| - return; |
| - } |
| - |
| - XkbStateRec status; |
| - XkbGetState(ui::GetXDisplay(), XkbUseCoreKbd, &status); |
| - if (out_caps_lock_enabled) { |
| - *out_caps_lock_enabled = status.locked_mods & LockMask; |
| - } |
| - if (out_num_lock_enabled) { |
| - *out_num_lock_enabled = status.locked_mods & num_lock_mask; |
| - } |
| -} |
| - |
| -// static |
| -bool XKeyboard::NumLockIsEnabled(unsigned int num_lock_mask) { |
| - bool num_lock_enabled = false; |
| - GetLockedModifiers(num_lock_mask, NULL /* Caps Lock */, &num_lock_enabled); |
| - return num_lock_enabled; |
| -} |
| - |
| -// static |
| -bool XKeyboard::CapsLockIsEnabled() { |
| - bool caps_lock_enabled = false; |
| - GetLockedModifiers(0, &caps_lock_enabled, NULL /* Num Lock */); |
| - return caps_lock_enabled; |
| -} |
| - |
| -// static |
| -bool XKeyboard::ContainsModifierKeyAsReplacement( |
| - const ModifierMap& modifier_map, ModifierKey key) { |
| - for (size_t i = 0; i < modifier_map.size(); ++i) { |
| - if (modifier_map[i].replacement == key) { |
| - return true; |
| - } |
| - } |
| - return false; |
| -} |
| - |
| -bool XKeyboard::SetCurrentKeyboardLayoutByName(const std::string& layout_name) { |
| +bool XKeyboardImpl::SetCurrentKeyboardLayoutByName( |
| + const std::string& layout_name) { |
| if (SetLayoutInternal(layout_name, current_modifier_map_, false)) { |
| current_layout_name_ = layout_name; |
| return true; |
| @@ -487,7 +482,7 @@ bool XKeyboard::SetCurrentKeyboardLayoutByName(const std::string& layout_name) { |
| return false; |
| } |
| -bool XKeyboard::ReapplyCurrentKeyboardLayout() { |
| +bool XKeyboardImpl::ReapplyCurrentKeyboardLayout() { |
| if (current_layout_name_.empty()) { |
| LOG(ERROR) << "Can't reapply XKB layout: layout unknown"; |
| return false; |
| @@ -496,12 +491,12 @@ bool XKeyboard::ReapplyCurrentKeyboardLayout() { |
| current_layout_name_, current_modifier_map_, true /* force */); |
| } |
| -void XKeyboard::ReapplyCurrentModifierLockStatus() { |
| +void XKeyboardImpl::ReapplyCurrentModifierLockStatus() { |
| SetLockedModifiers(current_caps_lock_status_ ? kEnableLock : kDisableLock, |
| current_num_lock_status_ ? kEnableLock : kDisableLock); |
| } |
| -bool XKeyboard::RemapModifierKeys(const ModifierMap& modifier_map) { |
| +bool XKeyboardImpl::RemapModifierKeys(const ModifierMap& modifier_map) { |
| const std::string layout_name = current_layout_name_.empty() ? |
| kDefaultLayoutName : current_layout_name_; |
| if (SetLayoutInternal(layout_name, modifier_map, false)) { |
| @@ -512,16 +507,16 @@ bool XKeyboard::RemapModifierKeys(const ModifierMap& modifier_map) { |
| return false; |
| } |
| -bool XKeyboard::KeepRightAlt(const std::string& xkb_layout_name) const { |
| +bool XKeyboardImpl::KeepRightAlt(const std::string& xkb_layout_name) const { |
| return keep_right_alt_xkb_layout_names_.count(xkb_layout_name) > 0; |
| } |
| -bool XKeyboard::KeepCapsLock(const std::string& xkb_layout_name) const { |
| +bool XKeyboardImpl::KeepCapsLock(const std::string& xkb_layout_name) const { |
| return caps_lock_remapped_xkb_layout_names_.count(xkb_layout_name) > 0; |
| } |
| // static |
| -std::string XKeyboard::ModifierKeyToString(ModifierKey key) { |
| +std::string XKeyboardImpl::ModifierKeyToString(ModifierKey key) { |
| switch (key) { |
| case kSearchKey: |
| return "search"; |
| @@ -539,5 +534,79 @@ std::string XKeyboard::ModifierKeyToString(ModifierKey key) { |
| return ""; |
| } |
| +// static |
| +void XKeyboardImpl::OnSetLayoutFinish(pid_t pid, |
| + int status, |
| + XKeyboardImpl* self) { |
| + CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| + VLOG(1) << "OnSetLayoutFinish: pid=" << pid; |
| + if (self->execute_queue_.empty()) { |
| + LOG(ERROR) << "OnSetLayoutFinish: execute_queue_ is empty. " |
| + << "base::LaunchProcess failed? pid=" << pid; |
| + return; |
| + } |
| + self->execute_queue_.pop(); |
| + self->MaybeExecuteSetLayoutCommand(); |
| +} |
| + |
| +} // namespace |
| + |
| +// static |
| +bool XKeyboard::SetAutoRepeatEnabled(bool enabled) { |
| + CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| + if (enabled) { |
| + XAutoRepeatOn(ui::GetXDisplay()); |
| + } else { |
| + XAutoRepeatOff(ui::GetXDisplay()); |
| + } |
| + DLOG(INFO) << "Set auto-repeat mode to: " << (enabled ? "on" : "off"); |
| + return true; |
| +} |
| + |
| +// static |
| +bool XKeyboard::SetAutoRepeatRate(const AutoRepeatRate& rate) { |
| + CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| + DLOG(INFO) << "Set auto-repeat rate to: " |
| + << rate.initial_delay_in_ms << " ms delay, " |
| + << rate.repeat_interval_in_ms << " ms interval"; |
| + if (XkbSetAutoRepeatRate(ui::GetXDisplay(), XkbUseCoreKbd, |
| + rate.initial_delay_in_ms, |
| + rate.repeat_interval_in_ms) != True) { |
| + LOG(ERROR) << "Failed to set auto-repeat rate"; |
| + return false; |
| + } |
| + return true; |
| +} |
| + |
| +// static |
| +bool XKeyboard::GetAutoRepeatEnabledForTesting() { |
| + XKeyboardState state = {}; |
| + XGetKeyboardControl(ui::GetXDisplay(), &state); |
| + return state.global_auto_repeat != AutoRepeatModeOff; |
| +} |
| + |
| +// static |
| +bool XKeyboard::GetAutoRepeatRateForTesting(AutoRepeatRate* out_rate) { |
| + return XkbGetAutoRepeatRate(ui::GetXDisplay(), XkbUseCoreKbd, |
| + &(out_rate->initial_delay_in_ms), |
| + &(out_rate->repeat_interval_in_ms)) == True; |
| +} |
| + |
| +// static |
| +bool XKeyboard::ContainsModifierKeyAsReplacement( |
| + const ModifierMap& modifier_map, ModifierKey key) { |
| + for (size_t i = 0; i < modifier_map.size(); ++i) { |
| + if (modifier_map[i].replacement == key) { |
| + return true; |
| + } |
| + } |
| + return false; |
| +} |
| + |
| +// static |
| +XKeyboard* XKeyboard::Create(const InputMethodUtil& util) { |
| + return new XKeyboardImpl(util); |
| +} |
| + |
| } // namespace input_method |
| } // namespace chromeos |