blob: 71496fd762a2baadec0d76afc55ef3c68497ac9b [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/ime/ime_controller_impl.h"
#include <vector>
#include "ash/ime/mode_indicator_observer.h"
#include "ash/ime/test_ime_controller_client.h"
#include "ash/public/cpp/ime_info.h"
#include "ash/shell.h"
#include "ash/system/ime/ime_observer.h"
#include "ash/system/tray/system_tray_notifier.h"
#include "ash/test/ash_test_base.h"
#include "base/strings/utf_string_conversions.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/base/ime/ash/extension_ime_util.h"
#include "ui/display/manager/display_manager.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/views/widget/widget.h"
namespace ash {
namespace {
// 43 is the designed size of the inner contents.
// This value corresponds with kMinSize defined in
// mode_indicator_delegate_view.cc.
const int kInnerSize = 43;
// Refreshes the IME list with fake IMEs and fake menu items.
void RefreshImesWithMenuItems(const std::string& current_ime_id,
const std::vector<std::string>& ime_ids,
const std::vector<std::string>& menu_item_keys) {
std::vector<ImeInfo> available_imes;
for (const std::string& ime_id : ime_ids) {
ImeInfo ime;
ime.id = ime_id;
available_imes.push_back(std::move(ime));
}
std::vector<ImeMenuItem> menu_items;
for (const std::string& menu_item_key : menu_item_keys) {
ImeMenuItem item;
item.key = menu_item_key;
menu_items.push_back(std::move(item));
}
Shell::Get()->ime_controller()->RefreshIme(
current_ime_id, std::move(available_imes), std::move(menu_items));
}
// Refreshes the IME list without adding any menu items.
void RefreshImes(const std::string& current_ime_id,
const std::vector<std::string>& ime_ids) {
const std::vector<std::string> empty_menu_items;
RefreshImesWithMenuItems(current_ime_id, ime_ids, empty_menu_items);
}
class TestImeObserver : public IMEObserver {
public:
TestImeObserver() = default;
~TestImeObserver() override = default;
// IMEObserver:
void OnIMERefresh() override { ++refresh_count_; }
void OnIMEMenuActivationChanged(bool is_active) override {
ime_menu_active_ = is_active;
}
int refresh_count_ = 0;
bool ime_menu_active_ = false;
};
class TestImeControllerObserver : public ImeControllerImpl::Observer {
public:
TestImeControllerObserver() = default;
TestImeControllerObserver(const TestImeControllerObserver&) = delete;
TestImeControllerObserver& operator=(const TestImeControllerObserver&) =
delete;
// IMEController::Observer:
void OnCapsLockChanged(bool enabled) override { ++caps_lock_count_; }
void OnKeyboardLayoutNameChanged(const std::string& layout_name) override {
last_keyboard_layout_name_ = layout_name;
}
const std::string& last_keyboard_layout_name() const {
return last_keyboard_layout_name_;
}
private:
int caps_lock_count_ = 0;
std::string last_keyboard_layout_name_;
};
using ImeControllerImplTest = AshTestBase;
TEST_F(ImeControllerImplTest, RefreshIme) {
ImeControllerImpl* controller = Shell::Get()->ime_controller();
TestImeObserver observer;
Shell::Get()->system_tray_notifier()->AddIMEObserver(&observer);
RefreshImesWithMenuItems("ime1", {"ime1", "ime2"}, {"menu1"});
// Cached data was updated.
EXPECT_EQ("ime1", controller->current_ime().id);
ASSERT_EQ(2u, controller->GetVisibleImes().size());
EXPECT_EQ("ime1", controller->GetVisibleImes()[0].id);
EXPECT_EQ("ime2", controller->GetVisibleImes()[1].id);
ASSERT_EQ(1u, controller->current_ime_menu_items().size());
EXPECT_EQ("menu1", controller->current_ime_menu_items()[0].key);
// Observer was notified.
EXPECT_EQ(1, observer.refresh_count_);
}
TEST_F(ImeControllerImplTest, NoCurrentIme) {
ImeControllerImpl* controller = Shell::Get()->ime_controller();
// Set up a single IME.
RefreshImes("ime1", {"ime1"});
EXPECT_EQ("ime1", controller->current_ime().id);
EXPECT_TRUE(controller->IsCurrentImeVisible());
// When there is no current IME the cached current IME is empty.
const std::string empty_ime_id;
RefreshImes(empty_ime_id, {"ime1"});
EXPECT_TRUE(controller->current_ime().id.empty());
}
TEST_F(ImeControllerImplTest, CurrentImeNotVisible) {
ImeControllerImpl* controller = Shell::Get()->ime_controller();
// Add only Dictation.
std::string dictation_id =
"_ext_ime_egfdjlfmgnehecnclamagfafdccgfndpdictation";
RefreshImes(dictation_id, {dictation_id});
EXPECT_EQ(dictation_id, controller->current_ime().id);
EXPECT_FALSE(controller->IsCurrentImeVisible());
EXPECT_EQ(0u, controller->GetVisibleImes().size());
// Add something else too, but Dictation is active.
RefreshImes(dictation_id, {dictation_id, "ime1"});
EXPECT_EQ(dictation_id, controller->current_ime().id);
EXPECT_FALSE(controller->IsCurrentImeVisible());
EXPECT_EQ(1u, controller->GetVisibleImes().size());
// Inactivate the other IME, leave Dictation in the list.
RefreshImes("ime1", {dictation_id, "ime1"});
EXPECT_EQ("ime1", controller->current_ime().id);
EXPECT_TRUE(controller->IsCurrentImeVisible());
EXPECT_EQ(1u, controller->GetVisibleImes().size());
}
TEST_F(ImeControllerImplTest, SetImesManagedByPolicy) {
ImeControllerImpl* controller = Shell::Get()->ime_controller();
TestImeObserver observer;
Shell::Get()->system_tray_notifier()->AddIMEObserver(&observer);
// Defaults to false.
EXPECT_FALSE(controller->managed_by_policy());
// Setting the value notifies observers.
controller->SetImesManagedByPolicy(true);
EXPECT_TRUE(controller->managed_by_policy());
EXPECT_EQ(1, observer.refresh_count_);
}
TEST_F(ImeControllerImplTest, ShowImeMenuOnShelf) {
ImeControllerImpl* controller = Shell::Get()->ime_controller();
TestImeObserver observer;
Shell::Get()->system_tray_notifier()->AddIMEObserver(&observer);
controller->ShowImeMenuOnShelf(true);
EXPECT_TRUE(observer.ime_menu_active_);
}
TEST_F(ImeControllerImplTest, CanSwitchIme) {
ImeControllerImpl* controller = Shell::Get()->ime_controller();
// Can't switch IMEs when none are available.
ASSERT_EQ(0u, controller->GetVisibleImes().size());
EXPECT_FALSE(controller->CanSwitchIme());
// Can't switch with only 1 IME.
RefreshImes("ime1", {"ime1"});
EXPECT_FALSE(controller->CanSwitchIme());
// Can switch with more than 1 IME.
RefreshImes("ime1", {"ime1", "ime2"});
EXPECT_TRUE(controller->CanSwitchIme());
}
TEST_F(ImeControllerImplTest, SwitchIme) {
ImeControllerImpl* controller = Shell::Get()->ime_controller();
TestImeControllerClient client;
// Can't switch IME before the client is set.
controller->SwitchToNextIme();
EXPECT_EQ(0, client.next_ime_count_);
controller->SwitchToLastUsedIme();
EXPECT_EQ(0, client.last_used_ime_count_);
controller->SwitchImeById("ime1", true /* show_message */);
EXPECT_EQ(0, client.switch_ime_count_);
// After setting the client the requests are forwarded.
controller->SetClient(&client);
controller->SwitchToNextIme();
EXPECT_EQ(1, client.next_ime_count_);
controller->SwitchToLastUsedIme();
EXPECT_EQ(1, client.last_used_ime_count_);
controller->SwitchImeById("ime1", true /* show_message */);
EXPECT_EQ(1, client.switch_ime_count_);
EXPECT_EQ("ime1", client.last_switch_ime_id_);
EXPECT_TRUE(client.last_show_message_);
controller->SwitchImeById("ime2", false /* show_message */);
EXPECT_EQ(2, client.switch_ime_count_);
EXPECT_EQ("ime2", client.last_switch_ime_id_);
EXPECT_FALSE(client.last_show_message_);
}
TEST_F(ImeControllerImplTest, SwitchImeWithAccelerator) {
ImeControllerImpl* controller = Shell::Get()->ime_controller();
TestImeControllerClient client;
controller->SetClient(&client);
const ui::Accelerator convert(ui::VKEY_CONVERT, ui::EF_NONE);
const ui::Accelerator non_convert(ui::VKEY_NONCONVERT, ui::EF_NONE);
const ui::Accelerator wide_half_1(ui::VKEY_DBE_SBCSCHAR, ui::EF_NONE);
const ui::Accelerator wide_half_2(ui::VKEY_DBE_DBCSCHAR, ui::EF_NONE);
// When there are no IMEs available switching by accelerator does not work.
ASSERT_EQ(0u, controller->GetVisibleImes().size());
EXPECT_FALSE(controller->CanSwitchImeWithAccelerator(convert));
EXPECT_FALSE(controller->CanSwitchImeWithAccelerator(non_convert));
EXPECT_FALSE(controller->CanSwitchImeWithAccelerator(wide_half_1));
EXPECT_FALSE(controller->CanSwitchImeWithAccelerator(wide_half_2));
// With only test IMEs (and no Japanese IMEs) the special keys do not work.
RefreshImes("ime1", {"ime1", "ime2"});
EXPECT_FALSE(controller->CanSwitchImeWithAccelerator(convert));
EXPECT_FALSE(controller->CanSwitchImeWithAccelerator(non_convert));
EXPECT_FALSE(controller->CanSwitchImeWithAccelerator(wide_half_1));
EXPECT_FALSE(controller->CanSwitchImeWithAccelerator(wide_half_2));
// Install both a test IME and Japanese IMEs.
using extension_ime_util::GetInputMethodIDByEngineID;
const std::string nacl_mozc_jp = GetInputMethodIDByEngineID("nacl_mozc_jp");
const std::string xkb_jp_jpn = GetInputMethodIDByEngineID("xkb:jp::jpn");
RefreshImes("ime1", {"ime1", nacl_mozc_jp, xkb_jp_jpn});
// Accelerator based switching now works.
EXPECT_TRUE(controller->CanSwitchImeWithAccelerator(convert));
EXPECT_TRUE(controller->CanSwitchImeWithAccelerator(non_convert));
EXPECT_TRUE(controller->CanSwitchImeWithAccelerator(wide_half_1));
EXPECT_TRUE(controller->CanSwitchImeWithAccelerator(wide_half_2));
// Convert keys jump directly to the requested IME.
controller->SwitchImeWithAccelerator(convert);
EXPECT_EQ(1, client.switch_ime_count_);
EXPECT_EQ(nacl_mozc_jp, client.last_switch_ime_id_);
controller->SwitchImeWithAccelerator(non_convert);
EXPECT_EQ(2, client.switch_ime_count_);
EXPECT_EQ(xkb_jp_jpn, client.last_switch_ime_id_);
// Switch from nacl_mozc_jp to xkb_jp_jpn.
RefreshImes(nacl_mozc_jp, {"ime1", nacl_mozc_jp, xkb_jp_jpn});
controller->SwitchImeWithAccelerator(wide_half_1);
EXPECT_EQ(3, client.switch_ime_count_);
EXPECT_EQ(xkb_jp_jpn, client.last_switch_ime_id_);
// Switch from xkb_jp_jpn to nacl_mozc_jp.
RefreshImes(xkb_jp_jpn, {"ime1", nacl_mozc_jp, xkb_jp_jpn});
controller->SwitchImeWithAccelerator(wide_half_2);
EXPECT_EQ(4, client.switch_ime_count_);
EXPECT_EQ(nacl_mozc_jp, client.last_switch_ime_id_);
}
TEST_F(ImeControllerImplTest, SetCapsLock) {
ImeControllerImpl* controller = Shell::Get()->ime_controller();
TestImeControllerClient client;
EXPECT_EQ(0, client.set_caps_lock_count_);
controller->SetCapsLockEnabled(true);
EXPECT_EQ(0, client.set_caps_lock_count_);
controller->SetClient(&client);
controller->SetCapsLockEnabled(true);
EXPECT_EQ(1, client.set_caps_lock_count_);
// Does not no-op when the state is the same. Should send all notifications.
controller->SetCapsLockEnabled(true);
EXPECT_EQ(2, client.set_caps_lock_count_);
controller->SetCapsLockEnabled(false);
EXPECT_EQ(3, client.set_caps_lock_count_);
controller->SetCapsLockEnabled(false);
EXPECT_EQ(4, client.set_caps_lock_count_);
EXPECT_FALSE(controller->IsCapsLockEnabled());
controller->UpdateCapsLockState(true);
EXPECT_TRUE(controller->IsCapsLockEnabled());
controller->UpdateCapsLockState(false);
EXPECT_FALSE(controller->IsCapsLockEnabled());
}
TEST_F(ImeControllerImplTest, OnKeyboardLayoutNameChanged) {
ImeControllerImpl* controller = Shell::Get()->ime_controller();
EXPECT_TRUE(controller->keyboard_layout_name().empty());
TestImeControllerObserver observer;
controller->AddObserver(&observer);
controller->OnKeyboardLayoutNameChanged("us(dvorak)");
EXPECT_EQ("us(dvorak)", controller->keyboard_layout_name());
EXPECT_EQ("us(dvorak)", observer.last_keyboard_layout_name());
}
TEST_F(ImeControllerImplTest, ShowModeIndicator) {
ImeControllerImpl* controller = Shell::Get()->ime_controller();
std::u16string text = u"US";
gfx::Rect cursor1_bounds(100, 100, 1, 20);
controller->ShowModeIndicator(cursor1_bounds, text);
views::Widget* widget1 =
controller->mode_indicator_observer()->active_widget();
EXPECT_TRUE(widget1);
// The widget bounds should be bigger than the inner size.
gfx::Rect bounds1 = widget1->GetWindowBoundsInScreen();
EXPECT_LE(kInnerSize, bounds1.width());
EXPECT_LE(kInnerSize, bounds1.height());
gfx::Rect cursor2_bounds(50, 200, 1, 20);
controller->ShowModeIndicator(cursor2_bounds, text);
views::Widget* widget2 =
controller->mode_indicator_observer()->active_widget();
EXPECT_TRUE(widget2);
EXPECT_NE(widget1, widget2);
// Check if the location of the mode indicator corresponds to the cursor
// bounds.
gfx::Rect bounds2 = widget2->GetWindowBoundsInScreen();
EXPECT_EQ(cursor1_bounds.x() - cursor2_bounds.x(), bounds1.x() - bounds2.x());
EXPECT_EQ(cursor1_bounds.y() - cursor2_bounds.y(), bounds1.y() - bounds2.y());
EXPECT_EQ(bounds1.width(), bounds2.width());
EXPECT_EQ(bounds1.height(), bounds2.height());
const gfx::Rect screen_bounds = display::Screen::GetScreen()
->GetDisplayMatching(cursor1_bounds)
.work_area();
const gfx::Rect cursor3_bounds(100, screen_bounds.bottom() - 25, 1, 20);
controller->ShowModeIndicator(cursor3_bounds, text);
views::Widget* widget3 =
controller->mode_indicator_observer()->active_widget();
EXPECT_TRUE(widget3);
EXPECT_NE(widget2, widget3);
// Check if the location of the mode indicator is considered with the screen
// size.
gfx::Rect bounds3 = widget3->GetWindowBoundsInScreen();
EXPECT_LT(bounds3.bottom(), screen_bounds.bottom());
}
} // namespace
} // namespace ash