blob: 532bbd2faa6d0df189be6b0b9f1dfafc2caf5ffa [file] [log] [blame]
// Copyright 2016 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 "ash/system/ime_menu/ime_menu_tray.h"
#include "ash/accessibility/accessibility_controller.h"
#include "ash/ime/ime_controller.h"
#include "ash/keyboard/ash_keyboard_controller.h"
#include "ash/keyboard/virtual_keyboard_controller.h"
#include "ash/public/cpp/ash_constants.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/root_window_controller.h"
#include "ash/session/session_controller.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/ime_menu/ime_list_view.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/tray/detailed_view_delegate.h"
#include "ash/system/tray/system_menu_button.h"
#include "ash/system/tray/system_tray_notifier.h"
#include "ash/system/tray/tray_constants.h"
#include "ash/system/tray/tray_container.h"
#include "ash/system/tray/tray_popup_item_style.h"
#include "ash/system/tray/tray_popup_utils.h"
#include "ash/system/tray/tray_utils.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/strings/utf_string_conversions.h"
#include "components/session_manager/session_manager_types.h"
#include "ui/base/ime/chromeos/extension_ime_util.h"
#include "ui/base/ime/ime_bridge.h"
#include "ui/base/ime/text_input_client.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/ui_base_features.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/range/range.h"
#include "ui/keyboard/keyboard_controller.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/separator.h"
#include "ui/views/layout/box_layout.h"
namespace ash {
namespace {
// Used for testing.
const int kEmojiButtonId = 1;
// Returns the height range of ImeListView.
gfx::Range GetImeListViewRange() {
const int max_items = 5;
const int min_items = 1;
const int tray_item_height = kTrayPopupItemMinHeight;
return gfx::Range(tray_item_height * min_items, tray_item_height * max_items);
}
// Shows language and input settings page.
void ShowIMESettings() {
base::RecordAction(base::UserMetricsAction("StatusArea_IME_Detailed"));
Shell::Get()->system_tray_model()->client_ptr()->ShowIMESettings();
}
// Returns true if the current screen is login or lock screen.
bool IsInLoginOrLockScreen() {
using session_manager::SessionState;
SessionState state = Shell::Get()->session_controller()->GetSessionState();
return state == SessionState::LOGIN_PRIMARY ||
state == SessionState::LOCKED ||
state == SessionState::LOGIN_SECONDARY;
}
// Returns true if the current input context type is password.
bool IsInPasswordInputContext() {
// Avoid getting IMEBridge instance if ash is not in browser.
// This is to temporarily mute the crash (http://crbug.com/867084).
// TODO(shuchen): This will be eventually be solved by the Mojo-based IMF.
return !::features::IsMultiProcessMash() &&
ui::IMEBridge::Get()->GetCurrentInputContext().type ==
ui::TEXT_INPUT_TYPE_PASSWORD;
}
class ImeMenuLabel : public views::Label {
public:
ImeMenuLabel() {
// Sometimes the label will be more than 2 characters, e.g. INTL and EXTD.
// This border makes sure we only leave room for ~2 and the others are
// truncated.
SetBorder(views::CreateEmptyBorder(gfx::Insets(0, 6)));
}
~ImeMenuLabel() override = default;
// views:Label:
gfx::Size CalculatePreferredSize() const override {
return gfx::Size(kTrayItemSize, kTrayItemSize);
}
int GetHeightForWidth(int width) const override { return kTrayItemSize; }
private:
DISALLOW_COPY_AND_ASSIGN(ImeMenuLabel);
};
class ImeMenuImageView : public views::ImageView {
public:
ImeMenuImageView() { SetBorder(views::CreateEmptyBorder(gfx::Insets(0, 6))); }
~ImeMenuImageView() override = default;
private:
DISALLOW_COPY_AND_ASSIGN(ImeMenuImageView);
};
SystemMenuButton* CreateImeMenuButton(views::ButtonListener* listener,
const gfx::VectorIcon& icon,
int accessible_name_id,
int right_border) {
return new SystemMenuButton(listener, icon, accessible_name_id);
}
// The view that contains IME menu title.
class ImeTitleView : public views::View, public views::ButtonListener {
public:
explicit ImeTitleView(bool show_settings_button) : settings_button_(nullptr) {
SetBorder(views::CreatePaddedBorder(
views::CreateSolidSidedBorder(0, 0, kMenuSeparatorWidth, 0,
kMenuSeparatorColor),
gfx::Insets(kMenuSeparatorVerticalPadding - kMenuSeparatorWidth, 0)));
auto box_layout =
std::make_unique<views::BoxLayout>(views::BoxLayout::kHorizontal);
box_layout->set_minimum_cross_axis_size(kTrayPopupItemMinHeight);
views::BoxLayout* layout_ptr = SetLayoutManager(std::move(box_layout));
auto* title_label =
new views::Label(l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_IME));
title_label->SetBorder(
views::CreateEmptyBorder(0, kMenuEdgeEffectivePadding, 1, 0));
title_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
TrayPopupItemStyle style(TrayPopupItemStyle::FontStyle::TITLE,
false /* use_unified_theme */);
style.SetupLabel(title_label);
AddChildView(title_label);
layout_ptr->SetFlexForView(title_label, 1);
if (show_settings_button) {
settings_button_ = CreateImeMenuButton(
this, kSystemMenuSettingsIcon, IDS_ASH_STATUS_TRAY_IME_SETTINGS, 0);
if (!TrayPopupUtils::CanOpenWebUISettings())
settings_button_->SetEnabled(false);
AddChildView(settings_button_);
}
}
// views::ButtonListener:
void ButtonPressed(views::Button* sender, const ui::Event& event) override {
DCHECK_EQ(sender, settings_button_);
ShowIMESettings();
}
~ImeTitleView() override = default;
private:
// Settings button that is only used if the emoji, handwriting and voice
// buttons are not available.
SystemMenuButton* settings_button_;
DISALLOW_COPY_AND_ASSIGN(ImeTitleView);
};
// The view that contains buttons shown on the bottom of IME menu.
class ImeButtonsView : public views::View, public views::ButtonListener {
public:
ImeButtonsView(ImeMenuTray* ime_menu_tray,
bool show_emoji,
bool show_handwriting,
bool show_voice)
: ime_menu_tray_(ime_menu_tray) {
DCHECK(ime_menu_tray_);
Init(show_emoji, show_handwriting, show_voice);
}
~ImeButtonsView() override = default;
// views::ButtonListener:
void ButtonPressed(views::Button* sender, const ui::Event& event) override {
if (sender == settings_button_) {
ime_menu_tray_->CloseBubble();
ShowIMESettings();
return;
}
// The |keyset| will be used for drawing input view keyset in IME
// extensions. ImeMenuTray::ShowKeyboardWithKeyset() will deal with
// the |keyset| string to generate the right input view url.
using chromeos::input_method::mojom::ImeKeyset;
ImeKeyset keyset = ImeKeyset::kNone;
if (sender == emoji_button_)
keyset = ImeKeyset::kEmoji;
else if (sender == voice_button_)
keyset = ImeKeyset::kVoice;
else if (sender == handwriting_button_)
keyset = ImeKeyset::kHandwriting;
else
NOTREACHED();
// TODO(dcheng): When https://crbug.com/742517 is fixed, Mojo will generate
// a constant for the number of values in the enum. For now, we just define
// it here and keep it in sync with the enum.
const int kImeKeysetUmaBoundary = 4;
UMA_HISTOGRAM_ENUMERATION("InputMethod.ImeMenu.EmojiHandwritingVoiceButton",
keyset, kImeKeysetUmaBoundary);
ime_menu_tray_->ShowKeyboardWithKeyset(keyset);
}
private:
void Init(bool show_emoji, bool show_handwriting, bool show_voice) {
auto box_layout =
std::make_unique<views::BoxLayout>(views::BoxLayout::kHorizontal);
box_layout->set_minimum_cross_axis_size(kTrayPopupItemMinHeight);
SetLayoutManager(std::move(box_layout));
SetBorder(views::CreatePaddedBorder(
views::CreateSolidSidedBorder(kMenuSeparatorWidth, 0, 0, 0,
kMenuSeparatorColor),
gfx::Insets(kMenuSeparatorVerticalPadding - kMenuSeparatorWidth,
kMenuExtraMarginFromLeftEdge)));
const int right_border = 1;
if (show_emoji) {
emoji_button_ =
CreateImeMenuButton(this, kImeMenuEmoticonIcon,
IDS_ASH_STATUS_TRAY_IME_EMOJI, right_border);
emoji_button_->set_id(kEmojiButtonId);
AddChildView(emoji_button_);
}
if (show_handwriting) {
handwriting_button_ = CreateImeMenuButton(
this, kImeMenuWriteIcon, IDS_ASH_STATUS_TRAY_IME_HANDWRITING,
right_border);
AddChildView(handwriting_button_);
}
if (show_voice) {
voice_button_ =
CreateImeMenuButton(this, kImeMenuMicrophoneIcon,
IDS_ASH_STATUS_TRAY_IME_VOICE, right_border);
AddChildView(voice_button_);
}
settings_button_ = CreateImeMenuButton(this, kSystemMenuSettingsIcon,
IDS_ASH_STATUS_TRAY_IME_SETTINGS, 0);
AddChildView(settings_button_);
if (!TrayPopupUtils::CanOpenWebUISettings())
settings_button_->SetEnabled(false);
}
ImeMenuTray* ime_menu_tray_;
SystemMenuButton* emoji_button_;
SystemMenuButton* handwriting_button_;
SystemMenuButton* voice_button_;
SystemMenuButton* settings_button_;
DISALLOW_COPY_AND_ASSIGN(ImeButtonsView);
};
// A list of available IMEs shown in the opt-in IME menu, which has a different
// height depending on the number of IMEs in the list.
class ImeMenuListView : public ImeListView {
public:
ImeMenuListView() : ImeMenuListView(std::make_unique<Delegate>()) {}
~ImeMenuListView() override = default;
private:
class Delegate : public DetailedViewDelegate {
public:
Delegate() : DetailedViewDelegate(nullptr /* tray_controller */) {}
// DetailedViewDelegate:
void TransitionToMainView(bool restore_focus) override {}
void CloseBubble() override {}
SkColor GetBackgroundColor(ui::NativeTheme* native_theme) override {
return native_theme->GetSystemColor(
ui::NativeTheme::kColorId_BubbleBackground);
}
bool IsOverflowIndicatorEnabled() const override { return true; }
private:
DISALLOW_COPY_AND_ASSIGN(Delegate);
};
ImeMenuListView(std::unique_ptr<Delegate> delegate)
: ImeListView(delegate.get(), false /* use_unified_theme */) {
set_should_focus_ime_after_selection_with_keyboard(true);
delegate_ = std::move(delegate);
}
// ImeListView:
void Layout() override {
gfx::Range height_range = GetImeListViewRange();
scroller()->ClipHeightTo(height_range.start(), height_range.end());
ImeListView::Layout();
}
std::unique_ptr<Delegate> delegate_;
DISALLOW_COPY_AND_ASSIGN(ImeMenuListView);
};
} // namespace
ImeMenuTray::ImeMenuTray(Shelf* shelf)
: TrayBackgroundView(shelf),
ime_controller_(Shell::Get()->ime_controller()),
label_(nullptr),
image_view_(nullptr),
keyboard_suppressed_(false),
show_bubble_after_keyboard_hidden_(false),
is_emoji_enabled_(false),
is_handwriting_enabled_(false),
is_voice_enabled_(false),
weak_ptr_factory_(this) {
DCHECK(ime_controller_);
SetInkDropMode(InkDropMode::ON);
CreateLabel();
SystemTrayNotifier* tray_notifier = Shell::Get()->system_tray_notifier();
tray_notifier->AddIMEObserver(this);
tray_notifier->AddVirtualKeyboardObserver(this);
// Show the tray even if virtual keyboard is shown. (Other tray buttons will
// be hidden).
set_show_with_virtual_keyboard(true);
}
ImeMenuTray::~ImeMenuTray() {
if (bubble_)
bubble_->bubble_view()->ResetDelegate();
SystemTrayNotifier* tray_notifier = Shell::Get()->system_tray_notifier();
tray_notifier->RemoveIMEObserver(this);
tray_notifier->RemoveVirtualKeyboardObserver(this);
auto* keyboard_controller = keyboard::KeyboardController::Get();
if (keyboard_controller->HasObserver(this))
keyboard_controller->RemoveObserver(this);
}
void ImeMenuTray::ShowImeMenuBubbleInternal(bool show_by_click) {
TrayBubbleView::InitParams init_params;
init_params.delegate = this;
init_params.parent_window = GetBubbleWindowContainer();
init_params.anchor_view = GetBubbleAnchor();
init_params.anchor_alignment = GetAnchorAlignment();
init_params.min_width = kTrayMenuWidth;
init_params.max_width = kTrayMenuWidth;
init_params.close_on_deactivate = true;
init_params.show_by_click = show_by_click;
TrayBubbleView* bubble_view = new TrayBubbleView(init_params);
bubble_view->set_anchor_view_insets(GetBubbleAnchorInsets());
// Add a title item with a separator on the top of the IME menu.
bool show_bottom_buttons = ShouldShowBottomButtons();
bubble_view->AddChildView(new ImeTitleView(!show_bottom_buttons));
// Adds IME list to the bubble.
ime_list_view_ = new ImeMenuListView();
ime_list_view_->Init(ShouldShowKeyboardToggle(),
ImeListView::SHOW_SINGLE_IME);
bubble_view->AddChildView(ime_list_view_);
if (show_bottom_buttons) {
bubble_view->AddChildView(new ImeButtonsView(
this, is_emoji_enabled_, is_handwriting_enabled_, is_voice_enabled_));
}
bubble_ = std::make_unique<TrayBubbleWrapper>(this, bubble_view,
false /* is_persistent */);
SetIsActive(true);
}
void ImeMenuTray::ShowKeyboardWithKeyset(
chromeos::input_method::mojom::ImeKeyset keyset) {
CloseBubble();
Shell::Get()
->ash_keyboard_controller()
->virtual_keyboard_controller()
->ForceShowKeyboardWithKeyset(keyset);
}
bool ImeMenuTray::ShouldShowBottomButtons() {
// Emoji, handwriting and voice input is not supported for these cases:
// 1) third party IME extensions.
// 2) login/lock screen.
// 3) password input client.
bool should_show_buttom_buttoms =
ime_controller_->is_extra_input_options_enabled() &&
!ime_controller_->current_ime().third_party && !IsInLoginOrLockScreen() &&
!IsInPasswordInputContext();
if (!should_show_buttom_buttoms) {
is_emoji_enabled_ = is_handwriting_enabled_ = is_voice_enabled_ = false;
return false;
}
is_emoji_enabled_ = ime_controller_->is_emoji_enabled();
is_handwriting_enabled_ = ime_controller_->is_handwriting_enabled();
is_voice_enabled_ = ime_controller_->is_voice_enabled();
return is_emoji_enabled_ || is_handwriting_enabled_ || is_voice_enabled_;
}
bool ImeMenuTray::ShouldShowKeyboardToggle() const {
return keyboard_suppressed_ &&
!Shell::Get()->accessibility_controller()->virtual_keyboard_enabled();
}
base::string16 ImeMenuTray::GetAccessibleNameForTray() {
return l10n_util::GetStringUTF16(IDS_ASH_IME_MENU_ACCESSIBLE_NAME);
}
void ImeMenuTray::HideBubbleWithView(const TrayBubbleView* bubble_view) {
if (bubble_->bubble_view() == bubble_view)
CloseBubble();
}
void ImeMenuTray::ClickedOutsideBubble() {
CloseBubble();
}
bool ImeMenuTray::PerformAction(const ui::Event& event) {
UserMetricsRecorder::RecordUserClickOnTray(
LoginMetricsRecorder::TrayClickTarget::kImeTray);
if (bubble_)
CloseBubble();
else
ShowBubble(event.IsMouseEvent() || event.IsGestureEvent());
return true;
}
void ImeMenuTray::CloseBubble() {
bubble_.reset();
ime_list_view_ = nullptr;
SetIsActive(false);
shelf()->UpdateAutoHideState();
}
void ImeMenuTray::ShowBubble(bool show_by_click) {
auto* keyboard_controller = keyboard::KeyboardController::Get();
if (keyboard_controller->IsKeyboardVisible()) {
show_bubble_after_keyboard_hidden_ = true;
keyboard_controller->AddObserver(this);
keyboard_controller->HideKeyboardExplicitlyBySystem();
} else {
base::RecordAction(base::UserMetricsAction("Tray_ImeMenu_Opened"));
ShowImeMenuBubbleInternal(show_by_click);
}
}
TrayBubbleView* ImeMenuTray::GetBubbleView() {
return bubble_ ? bubble_->bubble_view() : nullptr;
}
void ImeMenuTray::OnIMERefresh() {
UpdateTrayLabel();
if (bubble_ && ime_list_view_) {
ime_list_view_->Update(ime_controller_->current_ime().id,
ime_controller_->available_imes(),
ime_controller_->current_ime_menu_items(), false,
ImeListView::SHOW_SINGLE_IME);
}
}
void ImeMenuTray::OnIMEMenuActivationChanged(bool is_activated) {
SetVisible(is_activated);
if (is_activated)
UpdateTrayLabel();
else
CloseBubble();
}
base::string16 ImeMenuTray::GetAccessibleNameForBubble() {
return l10n_util::GetStringUTF16(IDS_ASH_IME_MENU_ACCESSIBLE_NAME);
}
bool ImeMenuTray::ShouldEnableExtraKeyboardAccessibility() {
return Shell::Get()->accessibility_controller()->spoken_feedback_enabled();
}
void ImeMenuTray::HideBubble(const TrayBubbleView* bubble_view) {
HideBubbleWithView(bubble_view);
}
void ImeMenuTray::OnKeyboardHidden(bool is_temporary_hide) {
if (show_bubble_after_keyboard_hidden_) {
show_bubble_after_keyboard_hidden_ = false;
auto* keyboard_controller = keyboard::KeyboardController::Get();
keyboard_controller->RemoveObserver(this);
ShowImeMenuBubbleInternal(false /* show_by_click */);
return;
}
}
void ImeMenuTray::OnKeyboardSuppressionChanged(bool suppressed) {
if (suppressed != keyboard_suppressed_ && bubble_)
CloseBubble();
keyboard_suppressed_ = suppressed;
}
void ImeMenuTray::UpdateTrayLabel() {
const mojom::ImeInfo& current_ime = ime_controller_->current_ime();
// For ARC IMEs, we use the globe icon instead of the short name of the active
// IME.
if (chromeos::extension_ime_util::IsArcIME(current_ime.id)) {
CreateImageView();
image_view_->SetImage(
gfx::CreateVectorIcon(kShelfGlobeIcon, kTrayIconSize, kTrayIconColor));
return;
}
// Updates the tray label based on the current input method.
CreateLabel();
if (current_ime.third_party)
label_->SetText(current_ime.short_name + base::UTF8ToUTF16("*"));
else
label_->SetText(current_ime.short_name);
}
void ImeMenuTray::CreateLabel() {
// Do nothing if label_ is already created.
if (label_)
return;
// Remove image_view_ at first if it's created.
if (image_view_) {
tray_container()->RemoveChildView(image_view_);
image_view_ = nullptr;
}
label_ = new ImeMenuLabel();
SetupLabelForTray(label_);
label_->SetElideBehavior(gfx::TRUNCATE);
label_->SetTooltipText(l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_IME));
tray_container()->AddChildView(label_);
}
void ImeMenuTray::CreateImageView() {
// Do nothing if image_view_ is already created.
if (image_view_)
return;
// Remove label_ at first if it's created.
if (label_) {
tray_container()->RemoveChildView(label_);
label_ = nullptr;
}
image_view_ = new ImeMenuImageView();
image_view_->set_tooltip_text(
l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_IME));
tray_container()->AddChildView(image_view_);
}
} // namespace ash