// Copyright (c) 2011 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/chromeos/input_method/xkeyboard.h"
#include <queue>
#include <utility>
#include <X11/XKBlib.h>
#include <X11/Xlib.h>
#include <glib.h>
#include <stdlib.h>
#include <string.h>
#include "base/memory/singleton.h"
#include "base/logging.h"
#include "base/string_util.h"
#include "base/process_util.h"
#include "chrome/browser/chromeos/cros/cros_library.h"
#include "chrome/browser/chromeos/input_method/input_method_util.h"
#include "content/browser/browser_thread.h"
namespace chromeos {
namespace input_method {
namespace {
// The default keyboard layout name in the xorg config file.
const char kDefaultLayoutName[] = "us";
// The command we use to set the current XKB layout and modifier key mapping.
// TODO(yusukes): Use libxkbfile.so instead of the command (crosbug.com/13105)
const char kSetxkbmapCommand[] = "/usr/bin/setxkbmap";
// See the comment at ModifierKey in the .h file.
ModifierKey kCustomizableKeys[] = {
kSearchKey,
kLeftControlKey,
kLeftAltKey
};
// These arrays are generated by 'gen_keyboard_overlay_data.py --altgr'
// These are the overlay names of layouts that shouldn't
// remap the right alt key.
const char* kKeepRightAltOverlays[] = {
"el",
"ca",
"it",
"iw",
"es_419",
"cs",
"et",
"es",
"en_US_altgr_intl",
"de_neo",
"nl",
"no",
"tr",
"lt",
"pt_PT",
"en_GB_dvorak",
"fr",
"bg",
"pt_BR",
"en_fr_hybrid_CA",
"hr",
"da",
"fi",
"fr_CA",
"ko",
"sv",
"sk",
"de",
"en_GB",
"pl",
"uk",
"sl",
"en_US_intl",
};
// These are the overlay names with caps lock remapped
const char* kCapsLockRemapped[] = {
"de_neo",
"en_US_colemak",
};
bool KeepRightAlt(const std::string& layout_name) {
for (size_t c = 0; c < arraysize(kKeepRightAltOverlays); ++c) {
if (GetKeyboardOverlayId(layout_name) == kKeepRightAltOverlays[c]) {
return true;
}
}
return false;
}
bool KeepCapsLock(const std::string& layout_name) {
for (size_t c = 0; c < arraysize(kCapsLockRemapped); ++c) {
if (GetKeyboardOverlayId(layout_name) == kCapsLockRemapped[c]) {
return true;
}
}
return false;
}
// This is a wrapper class around Display, that opens and closes X display in
// the constructor and destructor.
class ScopedDisplay {
public:
explicit ScopedDisplay(Display* display) : display_(display) {
if (!display_) {
LOG(ERROR) << "NULL display_ is passed";
}
}
~ScopedDisplay() {
if (display_) {
XCloseDisplay(display_);
}
}
Display* get() const {
return display_;
}
private:
Display* display_;
DISALLOW_COPY_AND_ASSIGN(ScopedDisplay);
};
// A singleton class which wraps the setxkbmap command.
class XKeyboard {
public:
// Returns the singleton instance of the class. Use LeakySingletonTraits.
// We don't delete the instance at exit.
static XKeyboard* GetInstance() {
return Singleton<XKeyboard, LeakySingletonTraits<XKeyboard> >::get();
}
// Sets the current keyboard layout to |layout_name|. This function does not
// change the current mapping of the modifier keys. Returns true on success.
bool SetLayout(const std::string& layout_name) {
if (SetLayoutInternal(layout_name, current_modifier_map_)) {
current_layout_name_ = layout_name;
return true;
}
return false;
}
// Remaps modifier keys. This function does not change the current keyboard
// layout. Returns true on success.
bool RemapModifierKeys(const ModifierMap& modifier_map) {
const std::string layout_name = current_layout_name_.empty() ?
kDefaultLayoutName : current_layout_name_;
if (SetLayoutInternal(layout_name, modifier_map)) {
current_layout_name_ = layout_name;
current_modifier_map_ = modifier_map;
return true;
}
return false;
}
// Turns on and off the auto-repeat of the keyboard. Returns true on success.
// TODO(yusukes): Remove this function.
bool SetAutoRepeatEnabled(bool enabled) {
ScopedDisplay display(XOpenDisplay(NULL));
if (!display.get()) {
return false;
}
if (enabled) {
XAutoRepeatOn(display.get());
} else {
XAutoRepeatOff(display.get());
}
DLOG(INFO) << "Set auto-repeat mode to: " << (enabled ? "on" : "off");
return true;
}
// Sets the auto-repeat rate of the keyboard, initial delay in ms, and repeat
// interval in ms. Returns true on success.
// TODO(yusukes): Call this function in non-UI thread or in an idle callback.
bool SetAutoRepeatRate(const AutoRepeatRate& rate) {
// TODO(yusukes): write auto tests for the function.
ScopedDisplay display(XOpenDisplay(NULL));
if (!display.get()) {
return false;
}
DLOG(INFO) << "Set auto-repeat rate to: "
<< rate.initial_delay_in_ms << " ms delay, "
<< rate.repeat_interval_in_ms << " ms interval";
if (XkbSetAutoRepeatRate(display.get(), XkbUseCoreKbd,
rate.initial_delay_in_ms,
rate.repeat_interval_in_ms) != True) {
LOG(ERROR) << "Failed to set auto-repeat rate";
return false;
}
return true;
}
private:
friend struct DefaultSingletonTraits<XKeyboard>;
XKeyboard() {
for (size_t i = 0; i < arraysize(kCustomizableKeys); ++i) {
ModifierKey key = kCustomizableKeys[i];
current_modifier_map_.push_back(ModifierKeyPair(key, key));
}
}
~XKeyboard() {
}
// 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) {
if (!CrosLibrary::Get()->EnsureLoaded()) {
// We should not try to change a layout inside ui_tests.
return false;
}
const std::string layout_to_set = CreateFullXkbLayoutName(
layout_name, modifier_map);
if (layout_to_set.empty()) {
return false;
}
if (!current_layout_name_.empty()) {
const std::string current_layout = CreateFullXkbLayoutName(
current_layout_name_, current_modifier_map_);
if (current_layout == layout_to_set) {
DLOG(INFO) << "The requested layout is already set: " << layout_to_set;
return true;
}
}
// Turn off caps lock if there is no kCapsLockKey in the remapped keys.
if (!ContainsModifierKeyAsReplacement(
modifier_map, kCapsLockKey)) {
SetCapsLockEnabled(false);
}
// TODO(yusukes): Revert to VLOG(1) when crosbug.com/15851 is resolved.
LOG(WARNING) << "Set layout: " << layout_to_set;
const bool start_execution = execute_queue_.empty();
// If no setxkbmap command is in flight (i.e. start_execution is true),
// start the first one by explicitly calling MaybeExecuteSetLayoutCommand().
// If one or more setxkbmap commands are already in flight, just push the
// layout name to the queue. setxkbmap command for the layout will be called
// via OnSetLayoutFinish() callback later.
execute_queue_.push(layout_to_set);
if (start_execution) {
MaybeExecuteSetLayoutCommand();
}
return true;
}
// 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() {
if (execute_queue_.empty()) {
return;
}
const std::string layout_to_set = execute_queue_.front();
std::vector<std::string> argv;
base::file_handle_mapping_vector fds_to_remap;
base::ProcessHandle handle = base::kNullProcessHandle;
argv.push_back(kSetxkbmapCommand);
argv.push_back("-layout");
argv.push_back(layout_to_set);
argv.push_back("-synch");
const bool result = base::LaunchApp(argv,
fds_to_remap, // No remapping.
false, // Don't wait.
&handle);
if (!result) {
LOG(ERROR) << "Failed to execute setxkbmap: " << layout_to_set;
execute_queue_ = std::queue<std::string>(); // clear the queue.
return;
}
// g_child_watch_add is necessary to prevent the process from becoming a
// zombie.
const base::ProcessId pid = base::GetProcId(handle);
g_child_watch_add(pid,
reinterpret_cast<GChildWatchFunc>(OnSetLayoutFinish),
this);
VLOG(1) << "ExecuteSetLayoutCommand: " << layout_to_set << ": pid=" << pid;
}
static void OnSetLayoutFinish(GPid pid, gint 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::LaunchApp failed? pid=" << pid;
return;
}
self->execute_queue_.pop();
self->MaybeExecuteSetLayoutCommand();
}
// 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_;
DISALLOW_COPY_AND_ASSIGN(XKeyboard);
};
} // namespace
std::string CreateFullXkbLayoutName(const std::string& layout_name,
const ModifierMap& modifier_map) {
static const char kValidLayoutNameCharacters[] =
"abcdefghijklmnopqrstuvwxyz0123456789()-_";
if (layout_name.empty()) {
LOG(ERROR) << "Invalid layout_name: " << layout_name;
return "";
}
if (layout_name.find_first_not_of(kValidLayoutNameCharacters) !=
std::string::npos) {
LOG(ERROR) << "Invalid layout_name: " << layout_name;
return "";
}
std::string use_search_key_as_str;
std::string use_left_control_key_as_str;
std::string use_left_alt_key_as_str;
for (size_t i = 0; i < modifier_map.size(); ++i) {
std::string* target = NULL;
switch (modifier_map[i].original) {
case kSearchKey:
target = &use_search_key_as_str;
break;
case kLeftControlKey:
target = &use_left_control_key_as_str;
break;
case kLeftAltKey:
target = &use_left_alt_key_as_str;
break;
default:
break;
}
if (!target) {
LOG(ERROR) << "We don't support remaping "
<< ModifierKeyToString(modifier_map[i].original);
return "";
}
if (!(target->empty())) {
LOG(ERROR) << ModifierKeyToString(modifier_map[i].original)
<< " appeared twice";
return "";
}
*target = ModifierKeyToString(modifier_map[i].replacement);
}
if (use_search_key_as_str.empty() ||
use_left_control_key_as_str.empty() ||
use_left_alt_key_as_str.empty()) {
LOG(ERROR) << "Incomplete ModifierMap: size=" << modifier_map.size();
return "";
}
if (KeepCapsLock(layout_name)) {
use_search_key_as_str = ModifierKeyToString(kSearchKey);
}
std::string full_xkb_layout_name =
StringPrintf("%s+chromeos(%s_%s_%s%s)", layout_name.c_str(),
use_search_key_as_str.c_str(),
use_left_control_key_as_str.c_str(),
use_left_alt_key_as_str.c_str(),
KeepRightAlt(layout_name) ? "_keepralt" : "");
if ((full_xkb_layout_name.substr(0, 3) != "us+") &&
(full_xkb_layout_name.substr(0, 3) != "us(")) {
full_xkb_layout_name += ",us";
}
return full_xkb_layout_name;
}
// This function is only for unittest.
bool CapsLockIsEnabled() {
ScopedDisplay display(XOpenDisplay(NULL));
if (!display.get()) {
return false;
}
XkbStateRec status;
XkbGetState(display.get(), XkbUseCoreKbd, &status);
return status.locked_mods & LockMask;
}
// TODO(yusukes): Call this function in non-UI thread or in an idle callback.
void SetCapsLockEnabled(bool enable_caps_lock) {
ScopedDisplay display(XOpenDisplay(NULL));
if (!display.get()) {
return;
}
XkbLockModifiers(
display.get(), XkbUseCoreKbd, LockMask, enable_caps_lock ? LockMask : 0);
}
bool 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 SetCurrentKeyboardLayoutByName(const std::string& layout_name) {
return XKeyboard::GetInstance()->SetLayout(layout_name);
}
bool RemapModifierKeys(const ModifierMap& modifier_map) {
return XKeyboard::GetInstance()->RemapModifierKeys(modifier_map);
}
bool SetAutoRepeatEnabled(bool enabled) {
return XKeyboard::GetInstance()->SetAutoRepeatEnabled(enabled);
}
bool SetAutoRepeatRate(const AutoRepeatRate& rate) {
return XKeyboard::GetInstance()->SetAutoRepeatRate(rate);
}
} // namespace input_method
} // namespace chromeos