// Copyright (c) 2012 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/candidate_window_controller_impl.h" #include #include #include "ash/shell.h" #include "ash/shell_window_ids.h" #include "ash/wm/window_animations.h" #include "base/logging.h" #include "base/memory/scoped_ptr.h" #include "base/observer_list.h" #include "chrome/browser/chromeos/input_method/candidate_window_view.h" #include "chrome/browser/chromeos/input_method/delayable_widget.h" #include "chrome/browser/chromeos/input_method/infolist_window_view.h" #include "chrome/browser/chromeos/input_method/mode_indicator_controller.h" #include "chrome/browser/chromeos/input_method/mode_indicator_widget.h" #include "ui/views/widget/widget.h" namespace chromeos { namespace input_method { namespace { // The milliseconds of the delay to show the infolist window. const int kInfolistShowDelayMilliSeconds = 500; // The milliseconds of the delay to hide the infolist window. const int kInfolistHideDelayMilliSeconds = 500; } // namespace CandidateWindowControllerImpl::CandidateWindowControllerImpl() : candidate_window_view_(NULL), latest_infolist_focused_index_(InfolistWindowView::InvalidFocusIndex()) { IBusBridge::Get()->SetCandidateWindowHandler(this); CreateView(); } CandidateWindowControllerImpl::~CandidateWindowControllerImpl() { IBusBridge::Get()->SetCandidateWindowHandler(NULL); candidate_window_view_->RemoveObserver(this); } void CandidateWindowControllerImpl::CreateView() { // Create a non-decorated frame. frame_.reset(new views::Widget); // The size is initially zero. views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP); // |frame_| and |infolist_window_| are owned by controller impl so // they should use WIDGET_OWNS_NATIVE_WIDGET ownership. params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; // Show the candidate window always on top params.parent = ash::Shell::GetContainer( ash::Shell::GetTargetRootWindow(), ash::internal::kShellWindowId_InputMethodContainer); frame_->Init(params); views::corewm::SetWindowVisibilityAnimationType( frame_->GetNativeView(), views::corewm::WINDOW_VISIBILITY_ANIMATION_TYPE_FADE); // Create the candidate window. candidate_window_view_ = new CandidateWindowView(frame_.get()); candidate_window_view_->Init(); candidate_window_view_->AddObserver(this); frame_->SetContentsView(candidate_window_view_); // Create the infolist window. infolist_window_.reset(new DelayableWidget); infolist_window_->Init(params); views::corewm::SetWindowVisibilityAnimationType( infolist_window_->GetNativeView(), views::corewm::WINDOW_VISIBILITY_ANIMATION_TYPE_FADE); InfolistWindowView* infolist_view = new InfolistWindowView; infolist_view->Init(); infolist_window_->SetContentsView(infolist_view); // Create the mode indicator controller. mode_indicator_controller_.reset( new ModeIndicatorController(new ModeIndicatorWidget)); } void CandidateWindowControllerImpl::Hide() { // To hide the candidate window we have to call HideLookupTable and // HideAuxiliaryText. Without calling HideAuxiliaryText the // auxiliary text area will remain. candidate_window_view_->HideLookupTable(); candidate_window_view_->HideAuxiliaryText(); infolist_window_->Hide(); } void CandidateWindowControllerImpl::SetCursorBounds( const gfx::Rect& cursor_bounds, const gfx::Rect& composition_head) { // A workaround for http://crosbug.com/6460. We should ignore very short Y // move to prevent the window from shaking up and down. const int kKeepPositionThreshold = 2; // px const gfx::Rect& last_bounds = candidate_window_view_->cursor_bounds(); const int delta_y = abs(last_bounds.y() - cursor_bounds.y()); if ((last_bounds.x() == cursor_bounds.x()) && (delta_y <= kKeepPositionThreshold)) { DVLOG(1) << "Ignored set_cursor_bounds signal to prevent window shake"; return; } // Remember the cursor bounds. candidate_window_view_->set_cursor_bounds(cursor_bounds); candidate_window_view_->set_composition_head_bounds(composition_head); // Move the window per the cursor bounds. candidate_window_view_->ResizeAndMoveParentFrame(); UpdateInfolistBounds(); // Mode indicator controller also needs the cursor bounds. mode_indicator_controller_->SetCursorBounds(cursor_bounds); } void CandidateWindowControllerImpl::UpdateAuxiliaryText( const std::string& utf8_text, bool visible) { // If it's not visible, hide the auxiliary text and return. if (!visible) { candidate_window_view_->HideAuxiliaryText(); return; } candidate_window_view_->UpdateAuxiliaryText(utf8_text); candidate_window_view_->ShowAuxiliaryText(); } void CandidateWindowControllerImpl::FocusStateChanged(bool is_focused) { mode_indicator_controller_->FocusStateChanged(is_focused); } // static void CandidateWindowControllerImpl::ConvertLookupTableToInfolistEntry( const CandidateWindow& candidate_window, std::vector* infolist_entries, size_t* focused_index) { DCHECK(focused_index); DCHECK(infolist_entries); *focused_index = InfolistWindowView::InvalidFocusIndex(); infolist_entries->clear(); const size_t cursor_index_in_page = candidate_window.cursor_position() % candidate_window.page_size(); for (size_t i = 0; i < candidate_window.candidates().size(); ++i) { const CandidateWindow::Entry& ibus_entry = candidate_window.candidates()[i]; if (ibus_entry.description_title.empty() && ibus_entry.description_body.empty()) continue; InfolistWindowView::Entry entry; entry.title = ibus_entry.description_title; entry.body = ibus_entry.description_body; infolist_entries->push_back(entry); if (i == cursor_index_in_page) *focused_index = infolist_entries->size() - 1; } } // static bool CandidateWindowControllerImpl::ShouldUpdateInfolist( const std::vector& old_entries, size_t old_focused_index, const std::vector& new_entries, size_t new_focused_index) { if (old_entries.empty() && new_entries.empty()) return false; if (old_entries.size() != new_entries.size()) return true; if (old_focused_index != new_focused_index) return true; for (size_t i = 0; i < old_entries.size(); ++i) { if (old_entries[i].title != new_entries[i].title || old_entries[i].body != new_entries[i].body ) { return true; } } return false; } void CandidateWindowControllerImpl::UpdateLookupTable( const CandidateWindow& candidate_window, bool visible) { // If it's not visible, hide the lookup table and return. if (!visible) { candidate_window_view_->HideLookupTable(); infolist_window_->Hide(); // TODO(nona): Introduce unittests for crbug.com/170036. latest_infolist_entries_.clear(); return; } candidate_window_view_->UpdateCandidates(candidate_window); candidate_window_view_->ShowLookupTable(); size_t focused_index = 0; std::vector infolist_entries; ConvertLookupTableToInfolistEntry(candidate_window, &infolist_entries, &focused_index); // If there is no infolist entry, just hide. if (infolist_entries.empty()) { infolist_window_->Hide(); return; } // If there is no change, just return. if (!ShouldUpdateInfolist(latest_infolist_entries_, latest_infolist_focused_index_, infolist_entries, focused_index)) { return; } latest_infolist_entries_ = infolist_entries; latest_infolist_focused_index_ = focused_index; InfolistWindowView* view = static_cast( infolist_window_->GetContentsView()); if (!view) { DLOG(ERROR) << "Contents View is not InfolistWindowView."; return; } view->Relayout(infolist_entries, focused_index); UpdateInfolistBounds(); if (focused_index < infolist_entries.size()) infolist_window_->DelayShow(kInfolistShowDelayMilliSeconds); else infolist_window_->DelayHide(kInfolistHideDelayMilliSeconds); } void CandidateWindowControllerImpl::UpdateInfolistBounds() { InfolistWindowView* view = static_cast( infolist_window_->GetContentsView()); if (!view) return; const gfx::Rect current_bounds = infolist_window_->GetClientAreaBoundsInScreen(); gfx::Rect new_bounds; new_bounds.set_size(view->GetPreferredSize()); new_bounds.set_origin(GetInfolistWindowPosition( frame_->GetClientAreaBoundsInScreen(), ash::Shell::GetScreen()->GetDisplayNearestWindow( infolist_window_->GetNativeView()).work_area(), new_bounds.size())); if (current_bounds != new_bounds) infolist_window_->SetBounds(new_bounds); } void CandidateWindowControllerImpl::UpdatePreeditText( const std::string& utf8_text, unsigned int cursor, bool visible) { // If it's not visible, hide the preedit text and return. if (!visible || utf8_text.empty()) { candidate_window_view_->HidePreeditText(); return; } candidate_window_view_->UpdatePreeditText(utf8_text); candidate_window_view_->ShowPreeditText(); } void CandidateWindowControllerImpl::OnCandidateCommitted(int index) { FOR_EACH_OBSERVER(CandidateWindowController::Observer, observers_, CandidateClicked(index)); } void CandidateWindowControllerImpl::OnCandidateWindowOpened() { FOR_EACH_OBSERVER(CandidateWindowController::Observer, observers_, CandidateWindowOpened()); } void CandidateWindowControllerImpl::OnCandidateWindowClosed() { FOR_EACH_OBSERVER(CandidateWindowController::Observer, observers_, CandidateWindowClosed()); } void CandidateWindowControllerImpl::AddObserver( CandidateWindowController::Observer* observer) { observers_.AddObserver(observer); } void CandidateWindowControllerImpl::RemoveObserver( CandidateWindowController::Observer* observer) { observers_.RemoveObserver(observer); } // static gfx::Point CandidateWindowControllerImpl::GetInfolistWindowPosition( const gfx::Rect& candidate_window_view_rect, const gfx::Rect& screen_rect, const gfx::Size& infolist_window_size) { gfx::Point result(candidate_window_view_rect.right(), candidate_window_view_rect.y()); if (candidate_window_view_rect.right() + infolist_window_size.width() > screen_rect.right()) result.set_x(candidate_window_view_rect.x() - infolist_window_size.width()); if (candidate_window_view_rect.y() + infolist_window_size.height() > screen_rect.bottom()) result.set_y(screen_rect.bottom() - infolist_window_size.height()); return result; } } // namespace input_method } // namespace chromeos