1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/browser/chromeos/input_method/candidate_window.h"
6
7 #include <algorithm>
8 #include <string>
9 #include <vector>
10
11 #include "base/logging.h"
12 #include "base/memory/scoped_ptr.h"
13 #include "base/observer_list.h"
14 #include "base/string_util.h"
15 #include "base/stringprintf.h"
16 #include "base/utf_string_conversions.h"
17 #include "third_party/cros/chromeos_input_method_ui.h"
18 #include "ui/gfx/canvas.h"
19 #include "ui/gfx/font.h"
20 #include "views/controls/label.h"
21 #include "views/controls/textfield/textfield.h"
22 #include "views/events/event.h"
23 #include "views/layout/fill_layout.h"
24 #include "views/layout/grid_layout.h"
25 #include "views/screen.h"
26 #include "views/widget/root_view.h"
27 #include "views/widget/widget.h"
28 #include "views/widget/widget_gtk.h"
29 #include "views/window/non_client_view.h"
30 #include "views/window/window.h"
31 #include "views/window/window_delegate.h"
32
33 namespace {
34
35 // Colors used in the candidate window UI.
36 const SkColor kFrameColor = SkColorSetRGB(0x96, 0x96, 0x96);
37 const SkColor kShortcutBackgroundColor = SkColorSetARGB(0x10, 0x3, 0x4, 0xf);
38 const SkColor kSelectedRowBackgroundColor = SkColorSetRGB(0xd1, 0xea, 0xff);
39 const SkColor kDefaultBackgroundColor = SkColorSetRGB(0xff, 0xff, 0xff);
40 const SkColor kSelectedRowFrameColor = SkColorSetRGB(0x7f, 0xac, 0xdd);
41 const SkColor kFooterTopColor = SkColorSetRGB(0xff, 0xff, 0xff);
42 const SkColor kFooterBottomColor = SkColorSetRGB(0xee, 0xee, 0xee);
43 const SkColor kShortcutColor = SkColorSetRGB(0x61, 0x61, 0x61);
44 const SkColor kDisabledShortcutColor = SkColorSetRGB(0xcc, 0xcc, 0xcc);
45 const SkColor kAnnotationColor = SkColorSetRGB(0x88, 0x88, 0x88);
46
47 // We'll use a bigger font size, so Chinese characters are more readable
48 // in the candidate window.
49 #if defined(CROS_FONTS_USING_BCI)
50 const int kFontSizeDelta = 1;
51 #else
52 const int kFontSizeDelta = 2;
53 #endif
54
55 // The minimum width of candidate labels in the vertical candidate
56 // window. We use this value to prevent the candidate window from being
57 // too narrow when all candidates are short.
58 const int kMinCandidateLabelWidth = 100;
59 // The maximum width of candidate labels in the vertical candidate
60 // window. We use this value to prevent the candidate window from being
61 // too wide when one of candidates are long.
62 const int kMaxCandidateLabelWidth = 500;
63
64 // VerticalCandidateLabel is used for rendering candidate text in
65 // the vertical candidate window.
66 class VerticalCandidateLabel : public views::Label {
~VerticalCandidateLabel()67 virtual ~VerticalCandidateLabel() {}
68
69 // Returns the preferred size, but guarantees that the width has at
70 // least kMinCandidateLabelWidth pixels.
GetPreferredSize()71 virtual gfx::Size GetPreferredSize() {
72 gfx::Size size = Label::GetPreferredSize();
73 // Hack. +2 is needed to prevent labels from getting elided like
74 // "abc..." in some cases. TODO(satorux): Figure out why it's
75 // necessary.
76 size.set_width(size.width() + 2);
77 if (size.width() < kMinCandidateLabelWidth) {
78 size.set_width(kMinCandidateLabelWidth);
79 }
80 if (size.width() > kMaxCandidateLabelWidth) {
81 size.set_width(kMaxCandidateLabelWidth);
82 }
83 return size;
84 }
85 };
86
87 // Wraps the given view with some padding, and returns it.
WrapWithPadding(views::View * view,const gfx::Insets & insets)88 views::View* WrapWithPadding(views::View* view, const gfx::Insets& insets) {
89 views::View* wrapper = new views::View;
90 // Use GridLayout to give some insets inside.
91 views::GridLayout* layout = new views::GridLayout(wrapper);
92 wrapper->SetLayoutManager(layout); // |wrapper| owns |layout|.
93 layout->SetInsets(insets);
94
95 views::ColumnSet* column_set = layout->AddColumnSet(0);
96 column_set->AddColumn(
97 views::GridLayout::FILL, views::GridLayout::FILL,
98 1, views::GridLayout::USE_PREF, 0, 0);
99 layout->StartRow(0, 0);
100
101 // Add the view contents.
102 layout->AddView(view); // |view| is owned by |wraper|, not |layout|.
103 return wrapper;
104 }
105
106 // Creates shortcut text from the given index and the orientation.
CreateShortcutText(int index,chromeos::InputMethodLookupTable::Orientation orientation)107 std::wstring CreateShortcutText(int index,
108 chromeos::InputMethodLookupTable::Orientation orientation) {
109 // Choose the character used for the shortcut label.
110 const wchar_t kShortcutCharacters[] = L"1234567890ABCDEF";
111 // The default character should not be used but just in case.
112 wchar_t shortcut_character = L'?';
113 // -1 to exclude the null character at the end.
114 if (index < static_cast<int>(arraysize(kShortcutCharacters) - 1)) {
115 shortcut_character = kShortcutCharacters[index];
116 }
117
118 std::wstring shortcut_text;
119 if (orientation == chromeos::InputMethodLookupTable::kVertical) {
120 shortcut_text = base::StringPrintf(L"%lc", shortcut_character);
121 } else {
122 shortcut_text = base::StringPrintf(L"%lc.", shortcut_character);
123 }
124
125 return shortcut_text;
126 }
127
128 // Creates the shortcut label, and returns it (never returns NULL).
129 // The label text is not set in this function.
CreateShortcutLabel(chromeos::InputMethodLookupTable::Orientation orientation)130 views::Label* CreateShortcutLabel(
131 chromeos::InputMethodLookupTable::Orientation orientation) {
132 // Create the shortcut label. The label will be owned by
133 // |wrapped_shortcut_label|, hence it's deleted when
134 // |wrapped_shortcut_label| is deleted.
135 views::Label* shortcut_label = new views::Label;
136
137 if (orientation == chromeos::InputMethodLookupTable::kVertical) {
138 shortcut_label->SetFont(
139 shortcut_label->font().DeriveFont(kFontSizeDelta, gfx::Font::BOLD));
140 } else {
141 shortcut_label->SetFont(
142 shortcut_label->font().DeriveFont(kFontSizeDelta));
143 }
144 // TODO(satorux): Maybe we need to use language specific fonts for
145 // candidate_label, like Chinese font for Chinese input method?
146 shortcut_label->SetColor(kShortcutColor);
147
148 return shortcut_label;
149 }
150
151 // Wraps the shortcut label, then decorates wrapped shortcut label
152 // and returns it (never returns NULL).
153 // The label text is not set in this function.
CreateWrappedShortcutLabel(views::Label * shortcut_label,chromeos::InputMethodLookupTable::Orientation orientation)154 views::View* CreateWrappedShortcutLabel(views::Label* shortcut_label,
155 chromeos::InputMethodLookupTable::Orientation orientation) {
156 // Wrap it with padding.
157 const gfx::Insets kVerticalShortcutLabelInsets(1, 6, 1, 6);
158 const gfx::Insets kHorizontalShortcutLabelInsets(1, 3, 1, 0);
159 const gfx::Insets insets =
160 (orientation == chromeos::InputMethodLookupTable::kVertical ?
161 kVerticalShortcutLabelInsets :
162 kHorizontalShortcutLabelInsets);
163 views::View* wrapped_shortcut_label =
164 WrapWithPadding(shortcut_label, insets);
165
166 // Add decoration based on the orientation.
167 if (orientation == chromeos::InputMethodLookupTable::kVertical) {
168 // Set the background color.
169 wrapped_shortcut_label->set_background(
170 views::Background::CreateSolidBackground(
171 kShortcutBackgroundColor));
172 }
173
174 return wrapped_shortcut_label;
175 }
176
177 // Creates the candidate label, and returns it (never returns NULL).
178 // The label text is not set in this function.
CreateCandidateLabel(chromeos::InputMethodLookupTable::Orientation orientation)179 views::Label* CreateCandidateLabel(
180 chromeos::InputMethodLookupTable::Orientation orientation) {
181 views::Label* candidate_label = NULL;
182
183 // Create the candidate label. The label will be added to |this| as a
184 // child view, hence it's deleted when |this| is deleted.
185 if (orientation == chromeos::InputMethodLookupTable::kVertical) {
186 candidate_label = new VerticalCandidateLabel;
187 } else {
188 candidate_label = new views::Label;
189 }
190
191 // Change the font size.
192 candidate_label->SetFont(
193 candidate_label->font().DeriveFont(kFontSizeDelta));
194 candidate_label->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
195
196 return candidate_label;
197 }
198
199 // Creates the annotation label, and return it (never returns NULL).
200 // The label text is not set in this function.
CreateAnnotationLabel(chromeos::InputMethodLookupTable::Orientation orientation)201 views::Label* CreateAnnotationLabel(
202 chromeos::InputMethodLookupTable::Orientation orientation) {
203 // Create the annotation label.
204 views::Label* annotation_label = new views::Label;
205
206 // Change the font size and color.
207 annotation_label->SetFont(
208 annotation_label->font().DeriveFont(kFontSizeDelta));
209 annotation_label->SetColor(kAnnotationColor);
210 annotation_label->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
211
212 return annotation_label;
213 }
214
215 // Computes shortcut column width.
ComputeShortcutColumnWidth(const chromeos::InputMethodLookupTable & lookup_table)216 int ComputeShortcutColumnWidth(
217 const chromeos::InputMethodLookupTable& lookup_table) {
218 int shortcut_column_width = 0;
219 // Create the shortcut label. The label will be owned by
220 // |wrapped_shortcut_label|, hence it's deleted when
221 // |wrapped_shortcut_label| is deleted.
222 views::Label* shortcut_label = CreateShortcutLabel(lookup_table.orientation);
223 scoped_ptr<views::View> wrapped_shortcut_label(
224 CreateWrappedShortcutLabel(shortcut_label, lookup_table.orientation));
225
226 // Compute the max width in shortcut labels.
227 // We'll create temporary shortcut labels, and choose the largest width.
228 for (int i = 0; i < lookup_table.page_size; ++i) {
229 shortcut_label->SetText(
230 CreateShortcutText(i, lookup_table.orientation));
231 shortcut_column_width =
232 std::max(shortcut_column_width,
233 wrapped_shortcut_label->GetPreferredSize().width());
234 }
235
236 return shortcut_column_width;
237 }
238
239 // Computes the page index. For instance, if the page size is 9, and the
240 // cursor is pointing to 13th candidate, the page index will be 1 (2nd
241 // page, as the index is zero-origin). Returns -1 on error.
ComputePageIndex(const chromeos::InputMethodLookupTable & lookup_table)242 int ComputePageIndex(const chromeos::InputMethodLookupTable& lookup_table) {
243 if (lookup_table.page_size > 0)
244 return lookup_table.cursor_absolute_index / lookup_table.page_size;
245 return -1;
246 }
247
248 // Computes candidate column width.
ComputeCandidateColumnWidth(const chromeos::InputMethodLookupTable & lookup_table)249 int ComputeCandidateColumnWidth(
250 const chromeos::InputMethodLookupTable& lookup_table) {
251 int candidate_column_width = 0;
252 scoped_ptr<views::Label> candidate_label(
253 CreateCandidateLabel(lookup_table.orientation));
254
255 // Compute the start index of |lookup_table_|.
256 const int current_page_index = ComputePageIndex(lookup_table);
257 if (current_page_index < 0)
258 return 0;
259 const size_t start_from = current_page_index * lookup_table.page_size;
260
261 // Compute the max width in candidate labels.
262 // We'll create temporary candidate labels, and choose the largest width.
263 for (size_t i = 0; i + start_from < lookup_table.candidates.size(); ++i) {
264 const size_t index = start_from + i;
265
266 candidate_label->SetText(
267 UTF8ToWide(lookup_table.candidates[index]));
268 candidate_column_width =
269 std::max(candidate_column_width,
270 candidate_label->GetPreferredSize().width());
271 }
272
273 return candidate_column_width;
274 }
275
276 // Computes annotation column width.
ComputeAnnotationColumnWidth(const chromeos::InputMethodLookupTable & lookup_table)277 int ComputeAnnotationColumnWidth(
278 const chromeos::InputMethodLookupTable& lookup_table) {
279 int annotation_column_width = 0;
280 scoped_ptr<views::Label> annotation_label(
281 CreateAnnotationLabel(lookup_table.orientation));
282
283 // Compute the start index of |lookup_table_|.
284 const int current_page_index = ComputePageIndex(lookup_table);
285 if (current_page_index < 0)
286 return 0;
287 const size_t start_from = current_page_index * lookup_table.page_size;
288
289 // Compute max width in annotation labels.
290 // We'll create temporary annotation labels, and choose the largest width.
291 for (size_t i = 0; i + start_from < lookup_table.annotations.size(); ++i) {
292 const size_t index = start_from + i;
293
294 annotation_label->SetText(
295 UTF8ToWide(lookup_table.annotations[index]));
296 annotation_column_width =
297 std::max(annotation_column_width,
298 annotation_label->GetPreferredSize().width());
299 }
300
301 return annotation_column_width;
302 }
303
304 } // namespace
305
306 namespace chromeos {
307
308 class CandidateView;
309
310 // CandidateWindowView is the main container of the candidate window UI.
311 class CandidateWindowView : public views::View {
312 public:
313 // The object can be monitored by the observer.
314 class Observer {
315 public:
~Observer()316 virtual ~Observer() {}
317 // The function is called when a candidate is committed.
318 // See comments at NotifyCandidateClicke() in chromeos_input_method_ui.h for
319 // details about the parameters.
320 virtual void OnCandidateCommitted(int index, int button, int flag) = 0;
321 };
322
323 explicit CandidateWindowView(views::Widget* parent_frame);
~CandidateWindowView()324 virtual ~CandidateWindowView() {}
325 void Init();
326
327 // Adds the given observer. The ownership is not transferred.
AddObserver(Observer * observer)328 void AddObserver(Observer* observer) {
329 observers_.AddObserver(observer);
330 }
331
332 // Removes the given observer.
RemoveObserver(Observer * observer)333 void RemoveObserver(Observer* observer) {
334 observers_.RemoveObserver(observer);
335 }
336
337 // Selects the candidate specified by the index in the current page
338 // (zero-origin). Changes the appearance of the selected candidate,
339 // updates the information in the candidate window as needed.
340 void SelectCandidateAt(int index_in_page);
341
342 // The function is called when a candidate is being dragged. From the
343 // given point, locates the candidate under the mouse cursor, and
344 // selects it.
345 void OnCandidateDragged(const gfx::Point& point);
346
347 // Commits the candidate currently being selected.
348 void CommitCandidate();
349
350 // Hides the lookup table.
351 void HideLookupTable();
352
353 // Hides the auxiliary text.
354 void HideAuxiliaryText();
355
356 // Shows the auxiliary text.
357 void ShowAuxiliaryText();
358
359 // Updates the auxiliary text.
360 void UpdateAuxiliaryText(const std::string& utf8_text);
361
362 // Returns true if we should update candidate views in the window. For
363 // instance, if we are going to show the same candidates as before, we
364 // don't have to update candidate views. This happens when the user just
365 // moves the cursor in the same page in the candidate window.
366 bool ShouldUpdateCandidateViews(
367 const InputMethodLookupTable& old_table,
368 const InputMethodLookupTable& new_table);
369
370 // Updates candidates of the candidate window from |lookup_table|.
371 // Candidates are arranged per |orientation|.
372 void UpdateCandidates(const InputMethodLookupTable& lookup_table);
373
374 // Resizes and moves the parent frame. The two actions should be
375 // performed consecutively as resizing may require the candidate window
376 // to move. For instance, we may need to move the candidate window from
377 // below the cursor to above the cursor, if the candidate window becomes
378 // too big to be shown near the bottom of the screen. This function
379 // needs to be called when the visible contents of the candidate window
380 // are modified.
381 void ResizeAndMoveParentFrame();
382
383 // Resizes the parent frame per the current contents size.
384 //
385 // The function is rarely used solely. See comments at
386 // ResizeAndMoveParentFrame().
387 void ResizeParentFrame();
388
389 // Moves the candidate window per the current cursor location, and the
390 // horizontal offset.
391 //
392 // The function is rarely used solely. See comments at
393 // ResizeAndMoveParentFrame().
394 void MoveParentFrame();
395
396 // Returns the horizontal offset used for placing the vertical candidate
397 // window so that the first candidate is aligned with the the text being
398 // converted like:
399 //
400 // XXX <- The user is converting XXX
401 // +-----+
402 // |1 XXX|
403 // |2 YYY|
404 // |3 ZZZ|
405 //
406 // Returns 0 if no candidate is present.
407 int GetHorizontalOffset();
408
409 // A function to be called when one of the |candidate_views_| receives a mouse
410 // press event.
411 void OnMousePressed();
412 // A function to be called when one of the |candidate_views_| receives a mouse
413 // release event.
414 void OnMouseReleased();
415
set_cursor_location(const gfx::Rect & cursor_location)416 void set_cursor_location(const gfx::Rect& cursor_location) {
417 cursor_location_ = cursor_location;
418 }
419
cursor_location() const420 const gfx::Rect& cursor_location() const { return cursor_location_; }
421
422 protected:
423 // Override View::VisibilityChanged()
424 virtual void VisibilityChanged(View* starting_from, bool is_visible) OVERRIDE;
425
426 // Override View::OnBoundsChanged()
427 virtual void OnBoundsChanged(const gfx::Rect& previous_bounds) OVERRIDE;
428
429 private:
430 // Initializes the candidate views if needed.
431 void MaybeInitializeCandidateViews(
432 const InputMethodLookupTable& lookup_table);
433
434 // Creates the footer area, where we show status information.
435 // For instance, we show a cursor position like 2/19.
436 views::View* CreateFooterArea();
437
438 // Creates the header area, where we show auxiliary text.
439 views::View* CreateHeaderArea();
440
441 // The lookup table (candidates).
442 InputMethodLookupTable lookup_table_;
443
444 // The index in the current page of the candidate currently being selected.
445 int selected_candidate_index_in_page_;
446
447 // The observers of the object.
448 ObserverList<Observer> observers_;
449
450 // The parent frame.
451 views::Widget* parent_frame_;
452
453 // Views created in the class will be part of tree of |this|, so these
454 // child views will be deleted when |this| is deleted.
455
456 // The candidate area is where candidates are rendered.
457 views::View* candidate_area_;
458 // The footer area is where the auxiliary text is shown, if the
459 // orientation is vertical. Usually the auxiliary text is used for
460 // showing candidate number information like 2/19.
461 views::View* footer_area_;
462 // We use this when we show something in the footer area.
463 scoped_ptr<views::View> footer_area_contents_;
464 // We use this when we show nothing in the footer area.
465 scoped_ptr<views::View> footer_area_place_holder_;
466 // The header area is where the auxiliary text is shown, if the
467 // orientation is horizontal. If the auxiliary text is not provided, we
468 // show nothing. For instance, we show pinyin text like "zhong'guo".
469 views::View* header_area_;
470 // We use this when we show something in the header area.
471 scoped_ptr<views::View> header_area_contents_;
472 // We use this when we show nothing in the header area.
473 scoped_ptr<views::View> header_area_place_holder_;
474 // The candidate views are used for rendering candidates.
475 std::vector<CandidateView*> candidate_views_;
476 // The header label is shown in the header area.
477 views::Label* header_label_;
478 // The footer label is shown in the footer area.
479 views::Label* footer_label_;
480
481 // Current columns width in |candidate_area_|.
482 int previous_shortcut_column_width_;
483 int previous_candidate_column_width_;
484 int previous_annotation_column_width_;
485
486 // The last cursor location.
487 gfx::Rect cursor_location_;
488
489 // true if a mouse button is pressed, and is not yet released.
490 bool mouse_is_pressed_;
491 };
492
493 // CandidateRow renderes a row of a candidate.
494 class CandidateView : public views::View {
495 public:
496 CandidateView(CandidateWindowView* parent_candidate_window,
497 int index_in_page,
498 InputMethodLookupTable::Orientation orientation);
~CandidateView()499 virtual ~CandidateView() {}
500 // Initializes the candidate view with the given column widths.
501 // A width of 0 means that the column is resizable.
502 void Init(int shortcut_column_width,
503 int candidate_column_width,
504 int annotation_column_width);
505
506 // Sets candidate text to the given text.
507 void SetCandidateText(const std::wstring& text);
508
509 // Sets shortcut text to the given text.
510 void SetShortcutText(const std::wstring& text);
511
512 // Sets annotation text to the given text.
513 void SetAnnotationText(const std::wstring& text);
514
515 // Selects the candidate row. Changes the appearance to make it look
516 // like a selected candidate.
517 void Select();
518
519 // Unselects the candidate row. Changes the appearance to make it look
520 // like an unselected candidate.
521 void Unselect();
522
523 // Enables or disables the candidate row based on |enabled|. Changes the
524 // appearance to make it look like unclickable area.
525 void SetRowEnabled(bool enabled);
526
527 // Returns the relative position of the candidate label.
528 gfx::Point GetCandidateLabelPosition() const;
529
530 private:
531 // Overridden from View:
532 virtual bool OnMousePressed(const views::MouseEvent& event) OVERRIDE;
533 virtual bool OnMouseDragged(const views::MouseEvent& event) OVERRIDE;
534 virtual void OnMouseReleased(const views::MouseEvent& event) OVERRIDE;
535 virtual void OnMouseCaptureLost() OVERRIDE;
536
537 // Zero-origin index in the current page.
538 int index_in_page_;
539
540 // The orientation of the candidate view.
541 InputMethodLookupTable::Orientation orientation_;
542
543 // The parent candidate window that contains this view.
544 CandidateWindowView* parent_candidate_window_;
545
546 // Views created in the class will be part of tree of |this|, so these
547 // child views will be deleted when |this| is deleted.
548
549 // The shortcut label renders shortcut numbers like 1, 2, and 3.
550 views::Label* shortcut_label_;
551 // The candidate label renders candidates.
552 views::Label* candidate_label_;
553 // The annotation label renders annotations.
554 views::Label* annotation_label_;
555 };
556
557 // The implementation of CandidateWindowController.
558 // CandidateWindowController controls the CandidateWindow.
559 class CandidateWindowController::Impl : public CandidateWindowView::Observer {
560 public:
561 Impl();
562 virtual ~Impl();
563
564 // Initializes the candidate window. Returns true on success.
565 bool Init();
566
567 private:
568 // CandidateWindowView::Observer implementation.
569 virtual void OnCandidateCommitted(int index,
570 int button,
571 int flags);
572
573 // Creates the candidate window view.
574 void CreateView();
575
576 // The function is called when |HideAuxiliaryText| signal is received in
577 // libcros. |input_method_library| is a void pointer to this object.
578 static void OnHideAuxiliaryText(void* input_method_library);
579
580 // The function is called when |HideLookupTable| signal is received in
581 // libcros. |input_method_library| is a void pointer to this object.
582 static void OnHideLookupTable(void* input_method_library);
583
584 // The function is called when |SetCursorLocation| signal is received
585 // in libcros. |input_method_library| is a void pointer to this object.
586 static void OnSetCursorLocation(void* input_method_library,
587 int x,
588 int y,
589 int width,
590 int height);
591
592 // The function is called when |UpdateAuxiliaryText| signal is received
593 // in libcros. |input_method_library| is a void pointer to this object.
594 static void OnUpdateAuxiliaryText(void* input_method_library,
595 const std::string& utf8_text,
596 bool visible);
597
598 // The function is called when |UpdateLookupTable| signal is received
599 // in libcros. |input_method_library| is a void pointer to this object.
600 static void OnUpdateLookupTable(void* input_method_library,
601 const InputMethodLookupTable& lookup_table);
602
603 // This function is called by libcros when ibus connects or disconnects.
604 // |input_method_library| is a void pointer to this object.
605 static void OnConnectionChange(void* input_method_library, bool connected);
606
607 // The connection is used for communicating with input method UI logic
608 // in libcros.
609 InputMethodUiStatusConnection* ui_status_connection_;
610
611 // The candidate window view.
612 CandidateWindowView* candidate_window_;
613
614 // This is the outer frame of the candidate window view. The frame will
615 // own |candidate_window_|.
616 scoped_ptr<views::Widget> frame_;
617 };
618
CandidateView(CandidateWindowView * parent_candidate_window,int index_in_page,InputMethodLookupTable::Orientation orientation)619 CandidateView::CandidateView(
620 CandidateWindowView* parent_candidate_window,
621 int index_in_page,
622 InputMethodLookupTable::Orientation orientation)
623 : index_in_page_(index_in_page),
624 orientation_(orientation),
625 parent_candidate_window_(parent_candidate_window),
626 shortcut_label_(NULL),
627 candidate_label_(NULL),
628 annotation_label_(NULL) {
629 }
630
Init(int shortcut_column_width,int candidate_column_width,int annotation_column_width)631 void CandidateView::Init(int shortcut_column_width,
632 int candidate_column_width,
633 int annotation_column_width) {
634 views::GridLayout* layout = new views::GridLayout(this);
635 SetLayoutManager(layout); // |this| owns |layout|.
636
637 // Create Labels.
638 shortcut_label_ = CreateShortcutLabel(orientation_);
639 views::View* wrapped_shortcut_label =
640 CreateWrappedShortcutLabel(shortcut_label_, orientation_);
641 candidate_label_ = CreateCandidateLabel(orientation_);
642 annotation_label_ = CreateAnnotationLabel(orientation_);
643
644 // Initialize the column set with three columns.
645 views::ColumnSet* column_set = layout->AddColumnSet(0);
646
647 // If orientation is vertical, each column width is fixed.
648 // Otherwise the width is resizable.
649 const views::GridLayout::SizeType column_type =
650 orientation_ == InputMethodLookupTable::kVertical ?
651 views::GridLayout::FIXED : views::GridLayout::USE_PREF;
652
653 const int padding_column_width =
654 orientation_ == InputMethodLookupTable::kVertical ? 4 : 6;
655
656 // Set shortcut column type and width.
657 column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL,
658 0, column_type, shortcut_column_width, 0);
659 column_set->AddPaddingColumn(0, padding_column_width);
660
661 // Set candidate column type and width.
662 column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL,
663 0, column_type, candidate_column_width, 0);
664 column_set->AddPaddingColumn(0, padding_column_width);
665
666 // Set annotation column type and width.
667 column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL,
668 0, column_type, annotation_column_width, 0);
669 column_set->AddPaddingColumn(0, padding_column_width);
670
671 // Add the shortcut label, the candidate label, and annotation label.
672 layout->StartRow(0, 0);
673 // |wrapped_shortcut_label|, |candidate_label_|, and |annotation_label_|
674 // will be owned by |this|.
675 layout->AddView(wrapped_shortcut_label);
676 layout->AddView(candidate_label_);
677 layout->AddView(annotation_label_);
678 }
679
SetCandidateText(const std::wstring & text)680 void CandidateView::SetCandidateText(const std::wstring& text) {
681 candidate_label_->SetText(text);
682 }
683
SetShortcutText(const std::wstring & text)684 void CandidateView::SetShortcutText(const std::wstring& text) {
685 shortcut_label_->SetText(text);
686 }
687
SetAnnotationText(const std::wstring & text)688 void CandidateView::SetAnnotationText(const std::wstring& text) {
689 annotation_label_->SetText(text);
690 }
691
Select()692 void CandidateView::Select() {
693 set_background(
694 views::Background::CreateSolidBackground(kSelectedRowBackgroundColor));
695 set_border(views::Border::CreateSolidBorder(1, kSelectedRowFrameColor));
696 // Need to call SchedulePaint() for background and border color changes.
697 SchedulePaint();
698 }
699
Unselect()700 void CandidateView::Unselect() {
701 set_background(NULL);
702 set_border(NULL);
703 SchedulePaint(); // See comments at Select().
704 }
705
SetRowEnabled(bool enabled)706 void CandidateView::SetRowEnabled(bool enabled) {
707 shortcut_label_->SetColor(
708 enabled ? kShortcutColor : kDisabledShortcutColor);
709 }
710
GetCandidateLabelPosition() const711 gfx::Point CandidateView::GetCandidateLabelPosition() const {
712 return candidate_label_->GetMirroredPosition();
713 }
714
OnMousePressed(const views::MouseEvent & event)715 bool CandidateView::OnMousePressed(const views::MouseEvent& event) {
716 parent_candidate_window_->OnMousePressed();
717 // Select the candidate. We'll commit the candidate when the mouse
718 // button is released.
719 parent_candidate_window_->SelectCandidateAt(index_in_page_);
720 // Request MouseDraggged and MouseReleased events.
721 return true;
722 }
723
OnMouseDragged(const views::MouseEvent & event)724 bool CandidateView::OnMouseDragged(const views::MouseEvent& event) {
725 gfx::Point location_in_candidate_window = event.location();
726 views::View::ConvertPointToView(this, parent_candidate_window_,
727 &location_in_candidate_window);
728 // Notify the candidate window that a candidate is now being dragged.
729 parent_candidate_window_->OnCandidateDragged(location_in_candidate_window);
730 // Request MouseReleased event.
731 return true;
732 }
733
OnMouseReleased(const views::MouseEvent & event)734 void CandidateView::OnMouseReleased(const views::MouseEvent& event) {
735 // Commit the current candidate.
736 parent_candidate_window_->CommitCandidate();
737 OnMouseCaptureLost();
738 }
739
OnMouseCaptureLost()740 void CandidateView::OnMouseCaptureLost() {
741 parent_candidate_window_->OnMouseReleased();
742 }
743
CandidateWindowView(views::Widget * parent_frame)744 CandidateWindowView::CandidateWindowView(
745 views::Widget* parent_frame)
746 : selected_candidate_index_in_page_(0),
747 parent_frame_(parent_frame),
748 candidate_area_(NULL),
749 footer_area_(NULL),
750 header_area_(NULL),
751 header_label_(NULL),
752 footer_label_(NULL),
753 previous_shortcut_column_width_(0),
754 previous_candidate_column_width_(0),
755 previous_annotation_column_width_(0),
756 mouse_is_pressed_(false) {
757 }
758
Init()759 void CandidateWindowView::Init() {
760 // Set the background and the border of the view.
761 set_background(
762 views::Background::CreateSolidBackground(kDefaultBackgroundColor));
763 set_border(views::Border::CreateSolidBorder(1, kFrameColor));
764
765 // Create the header area.
766 header_area_ = CreateHeaderArea();
767 // Create the candidate area.
768 candidate_area_ = new views::View;
769 // Create the footer area.
770 footer_area_ = CreateFooterArea();
771
772 // Set the window layout of the view
773 views::GridLayout* layout = new views::GridLayout(this);
774 SetLayoutManager(layout); // |this| owns layout|.
775 views::ColumnSet* column_set = layout->AddColumnSet(0);
776 column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL,
777 0, views::GridLayout::USE_PREF, 0, 0);
778
779 // Add the header area.
780 layout->StartRow(0, 0);
781 layout->AddView(header_area_); // |header_area_| is owned by |this|.
782
783 // Add the candidate area.
784 layout->StartRow(0, 0);
785 layout->AddView(candidate_area_); // |candidate_area_| is owned by |this|.
786
787 // Add the footer area.
788 layout->StartRow(0, 0);
789 layout->AddView(footer_area_); // |footer_area_| is owned by |this|.
790 }
791
HideLookupTable()792 void CandidateWindowView::HideLookupTable() {
793 if (!mouse_is_pressed_) {
794 parent_frame_->Hide();
795 return;
796 }
797
798 // We should not hide the |frame_| when a mouse is pressed, so we don't run
799 // into issues below.
800 //
801 // First, in the following scenario, it seems that the Views popup window does
802 // not release mouse/keyboard grab even after it gets hidden.
803 //
804 // 1. create a popup window by views::Widget::CreateWidget() with the
805 // accept_events flag set to true on the CreateParams.
806 // 2. press a mouse button on the window.
807 // 3. before releasing the mouse button, Hide() the window.
808 // 4. release the button.
809 //
810 // And if we embed IME candidate window into Chrome, the window sometimes
811 // receives an extra 'hide-lookup-table' event before mouse button is
812 // released:
813 //
814 // 1. the candidate window is clicked.
815 // 2. The mouse click handler in this file, OnMousePressed() in CandidateView,
816 // is called, and the handler consumes the event by returning true.
817 // 3. HOWEVER, if the candidate window is embedded into Chrome, the event is
818 // also sent to Chrome! (problem #1)
819 // 4. im-ibus.so in Chrome sends 'focus-out' event to ibus-daemon.
820 // 5. ibus-daemon sends 'hide-lookup-table' event to the candidate window.
821 // 6. the window is hidden, but the window does not release mouse/keyboard
822 // grab! (problem #2)
823 // 7. mouse button is released.
824 // 8. now all mouse/keyboard events are consumed by the hidden popup, and are
825 // not sent to Chrome.
826 //
827 // TODO(yusukes): investigate why the click event is sent to both candidate
828 // window and Chrome. http://crosbug.com/11423
829 // TODO(yusukes): investigate if we could fix Views so it always releases grab
830 // when a popup window gets hidden. http://crosbug.com/11422
831 //
832 LOG(WARNING) << "Can't hide the table since a mouse button is not released.";
833 }
834
OnMousePressed()835 void CandidateWindowView::OnMousePressed() {
836 mouse_is_pressed_ = true;
837 }
838
OnMouseReleased()839 void CandidateWindowView::OnMouseReleased() {
840 mouse_is_pressed_ = false;
841 }
842
HideAuxiliaryText()843 void CandidateWindowView::HideAuxiliaryText() {
844 views::View* target_area = (
845 lookup_table_.orientation == InputMethodLookupTable::kHorizontal ?
846 header_area_ : footer_area_);
847 views::View* target_place_holder = (
848 lookup_table_.orientation == InputMethodLookupTable::kHorizontal ?
849 header_area_place_holder_.get() :
850 footer_area_place_holder_.get());
851 // Put the place holder to the target display area.
852 target_area->RemoveAllChildViews(false); // Don't delete child views.
853 target_area->AddChildView(target_place_holder);
854 }
855
ShowAuxiliaryText()856 void CandidateWindowView::ShowAuxiliaryText() {
857 views::View* target_area = (
858 lookup_table_.orientation == InputMethodLookupTable::kHorizontal ?
859 header_area_ : footer_area_);
860 views::View* target_contents = (
861 lookup_table_.orientation == InputMethodLookupTable::kHorizontal ?
862 header_area_contents_.get() :
863 footer_area_contents_.get());
864
865 if (target_contents->parent() != target_area) {
866 // If contents not in display area, put it in.
867 target_area->RemoveAllChildViews(false); // Don't delete child views.
868 target_area->AddChildView(target_contents);
869 }
870 }
871
UpdateAuxiliaryText(const std::string & utf8_text)872 void CandidateWindowView::UpdateAuxiliaryText(const std::string& utf8_text) {
873 views::Label* target_label = (
874 lookup_table_.orientation == InputMethodLookupTable::kHorizontal ?
875 header_label_ : footer_label_);
876 target_label->SetText(UTF8ToWide(utf8_text));
877 }
878
ShouldUpdateCandidateViews(const InputMethodLookupTable & old_table,const InputMethodLookupTable & new_table)879 bool CandidateWindowView::ShouldUpdateCandidateViews(
880 const InputMethodLookupTable& old_table,
881 const InputMethodLookupTable& new_table) {
882 // Check if most table contents are identical.
883 if (old_table.page_size == new_table.page_size &&
884 old_table.orientation == new_table.orientation &&
885 old_table.candidates == new_table.candidates &&
886 old_table.labels == new_table.labels &&
887 old_table.annotations == new_table.annotations &&
888 // Check if the page indexes are identical.
889 ComputePageIndex(old_table) == ComputePageIndex(new_table)) {
890 // If all of the conditions are met, we don't have to update candidate
891 // views.
892 return false;
893 }
894 return true;
895 }
896
UpdateCandidates(const InputMethodLookupTable & new_lookup_table)897 void CandidateWindowView::UpdateCandidates(
898 const InputMethodLookupTable& new_lookup_table) {
899 const bool should_update = ShouldUpdateCandidateViews(lookup_table_,
900 new_lookup_table);
901 // Updating the candidate views is expensive. We'll skip this if possible.
902 if (should_update) {
903 // Initialize candidate views if necessary.
904 MaybeInitializeCandidateViews(new_lookup_table);
905
906 // Compute the index of the current page.
907 const int current_page_index = ComputePageIndex(new_lookup_table);
908 if (current_page_index < 0) {
909 LOG(ERROR) << "Invalid lookup_table: " << new_lookup_table.ToString();
910 return;
911 }
912
913 // Update the candidates in the current page.
914 const size_t start_from = current_page_index * new_lookup_table.page_size;
915
916 // In some cases, engines send empty shortcut labels. For instance,
917 // ibus-mozc sends empty labels when they show suggestions. In this
918 // case, we should not show shortcut labels.
919 const bool no_shortcut_mode =
920 (start_from < new_lookup_table.labels.size() &&
921 new_lookup_table.labels[start_from] == "");
922 for (size_t i = 0; i < candidate_views_.size(); ++i) {
923 const size_t index_in_page = i;
924 const size_t candidate_index = start_from + index_in_page;
925 CandidateView* candidate_view = candidate_views_[index_in_page];
926 // Set the shortcut text.
927 if (no_shortcut_mode) {
928 candidate_view->SetShortcutText(L"");
929 } else {
930 // At this moment, we don't use labels sent from engines for UX
931 // reasons. First, we want to show shortcut labels in empty rows
932 // (ex. show 6, 7, 8, ... in empty rows when the number of
933 // candidates is 5). Second, we want to add a period after each
934 // shortcut label when the candidate window is horizontal.
935 candidate_view->SetShortcutText(
936 CreateShortcutText(i, new_lookup_table.orientation));
937 }
938 // Set the candidate text.
939 if (candidate_index < new_lookup_table.candidates.size() &&
940 candidate_index < new_lookup_table.annotations.size()) {
941 candidate_view->SetCandidateText(
942 UTF8ToWide(new_lookup_table.candidates[candidate_index]));
943 candidate_view->SetAnnotationText(
944 UTF8ToWide(new_lookup_table.annotations[candidate_index]));
945 candidate_view->SetRowEnabled(true);
946 } else {
947 // Disable the empty row.
948 candidate_view->SetCandidateText(L"");
949 candidate_view->SetAnnotationText(L"");
950 candidate_view->SetRowEnabled(false);
951 }
952 }
953 }
954 // Update the current lookup table. We'll use lookup_table_ from here.
955 // Note that SelectCandidateAt() uses lookup_table_.
956 lookup_table_ = new_lookup_table;
957
958 // Select the current candidate in the page.
959 const int current_candidate_in_page =
960 lookup_table_.cursor_absolute_index % lookup_table_.page_size;
961 SelectCandidateAt(current_candidate_in_page);
962 }
963
MaybeInitializeCandidateViews(const InputMethodLookupTable & lookup_table)964 void CandidateWindowView::MaybeInitializeCandidateViews(
965 const InputMethodLookupTable& lookup_table) {
966 const InputMethodLookupTable::Orientation orientation =
967 lookup_table.orientation;
968 const int page_size = lookup_table.page_size;
969
970 // Current column width.
971 int shortcut_column_width = 0;
972 int candidate_column_width = 0;
973 int annotation_column_width = 0;
974
975 // If orientation is horizontal, don't need to compute width,
976 // because each label is left aligned.
977 if (orientation == InputMethodLookupTable::kVertical) {
978 shortcut_column_width = ComputeShortcutColumnWidth(lookup_table);
979 candidate_column_width = ComputeCandidateColumnWidth(lookup_table);
980 annotation_column_width = ComputeAnnotationColumnWidth(lookup_table);
981 }
982
983 // If the requested number of views matches the number of current views, and
984 // previous and current column width are same, just reuse these.
985 //
986 // Note that the early exit logic is not only useful for improving
987 // performance, but also necessary for the horizontal candidate window
988 // to be redrawn properly. If we get rid of the logic, the horizontal
989 // candidate window won't get redrawn properly for some reason when
990 // there is no size change. You can test this by removing "return" here
991 // and type "ni" with Pinyin input method.
992 if (static_cast<int>(candidate_views_.size()) == page_size &&
993 lookup_table_.orientation == orientation &&
994 previous_shortcut_column_width_ == shortcut_column_width &&
995 previous_candidate_column_width_ == candidate_column_width &&
996 previous_annotation_column_width_ == annotation_column_width) {
997 return;
998 }
999
1000 // Update the previous column widths.
1001 previous_shortcut_column_width_ = shortcut_column_width;
1002 previous_candidate_column_width_ = candidate_column_width;
1003 previous_annotation_column_width_ = annotation_column_width;
1004
1005 // Clear the existing candidate_views if any.
1006 for (size_t i = 0; i < candidate_views_.size(); ++i) {
1007 candidate_area_->RemoveChildView(candidate_views_[i]);
1008 }
1009 candidate_views_.clear();
1010
1011 views::GridLayout* layout = new views::GridLayout(candidate_area_);
1012 // |candidate_area_| owns |layout|.
1013 candidate_area_->SetLayoutManager(layout);
1014 // Initialize the column set.
1015 views::ColumnSet* column_set = layout->AddColumnSet(0);
1016 if (orientation == InputMethodLookupTable::kVertical) {
1017 column_set->AddColumn(views::GridLayout::FILL,
1018 views::GridLayout::FILL,
1019 0, views::GridLayout::USE_PREF, 0, 0);
1020 } else {
1021 for (int i = 0; i < page_size; ++i) {
1022 column_set->AddColumn(views::GridLayout::FILL,
1023 views::GridLayout::FILL,
1024 0, views::GridLayout::USE_PREF, 0, 0);
1025 }
1026 }
1027
1028 // Set insets so the border of the selected candidate is drawn inside of
1029 // the border of the main candidate window, but we don't have the inset
1030 // at the top and the bottom as we have the borders of the header and
1031 // footer areas.
1032 const gfx::Insets kCandidateAreaInsets(0, 1, 0, 1);
1033 layout->SetInsets(kCandidateAreaInsets.top(),
1034 kCandidateAreaInsets.left(),
1035 kCandidateAreaInsets.bottom(),
1036 kCandidateAreaInsets.right());
1037
1038 // Add views to the candidate area.
1039 if (orientation == InputMethodLookupTable::kHorizontal) {
1040 layout->StartRow(0, 0);
1041 }
1042
1043 for (int i = 0; i < page_size; ++i) {
1044 CandidateView* candidate_row = new CandidateView(this, i, orientation);
1045 candidate_row->Init(shortcut_column_width,
1046 candidate_column_width,
1047 annotation_column_width);
1048 candidate_views_.push_back(candidate_row);
1049 if (orientation == InputMethodLookupTable::kVertical) {
1050 layout->StartRow(0, 0);
1051 }
1052 // |candidate_row| will be owned by |candidate_area_|.
1053 layout->AddView(candidate_row);
1054 }
1055
1056 // Compute views size in |layout|.
1057 // If we don't call this function, GetHorizontalOffset() often
1058 // returns invalid value (returns 0), then candidate window
1059 // moves right from the correct position in MoveParentFrame().
1060 // TODO(nhiroki): Figure out why it returns invalid value.
1061 // It seems that the x-position of the candidate labels is not set.
1062 layout->Layout(candidate_area_);
1063 }
1064
CreateHeaderArea()1065 views::View* CandidateWindowView::CreateHeaderArea() {
1066 // |header_area_place_holder_| will not be owned by another view.
1067 // This will be deleted by scoped_ptr.
1068 //
1069 // This is because we swap the contents of |header_area_| between
1070 // |header_area_place_holder_| (to show nothing) and
1071 // |header_area_contents_| (to show something). In other words,
1072 // |header_area_| only contains one of the two views hence cannot own
1073 // the two views at the same time.
1074 header_area_place_holder_.reset(new views::View);
1075 header_area_place_holder_->set_parent_owned(false); // Won't be owened.
1076
1077 // |header_label_| will be owned by |header_area_contents_|.
1078 header_label_ = new views::Label;
1079 header_label_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
1080
1081 const gfx::Insets kHeaderInsets(2, 2, 2, 4);
1082 // |header_area_contents_| will not be owned by another view.
1083 // See a comment at |header_area_place_holder_| for why.
1084 header_area_contents_.reset(
1085 WrapWithPadding(header_label_, kHeaderInsets));
1086 header_area_contents_->set_parent_owned(false); // Won't be owened.
1087 header_area_contents_->set_border(
1088 views::Border::CreateSolidBorder(1, kFrameColor));
1089 header_area_contents_->set_background(
1090 views::Background::CreateVerticalGradientBackground(
1091 kFooterTopColor,
1092 kFooterBottomColor));
1093
1094 views::View* header_area = new views::View;
1095 header_area->SetLayoutManager(new views::FillLayout);
1096 // Initialize the header area with the place holder (i.e. show nothing).
1097 header_area->AddChildView(header_area_place_holder_.get());
1098 return header_area;
1099 }
1100
CreateFooterArea()1101 views::View* CandidateWindowView::CreateFooterArea() {
1102 // |footer_area_place_holder_| will not be owned by another view.
1103 // See also the comment about |header_area_place_holder_| in
1104 // CreateHeaderArea().
1105 footer_area_place_holder_.reset(new views::View);
1106 footer_area_place_holder_->set_parent_owned(false); // Won't be owened.
1107
1108 footer_label_ = new views::Label();
1109 footer_label_->SetHorizontalAlignment(views::Label::ALIGN_RIGHT);
1110
1111 const gfx::Insets kFooterInsets(2, 2, 2, 4);
1112 footer_area_contents_.reset(
1113 WrapWithPadding(footer_label_, kFooterInsets));
1114 footer_area_contents_->set_parent_owned(false); // Won't be owened.
1115 footer_area_contents_->set_border(
1116 views::Border::CreateSolidBorder(1, kFrameColor));
1117 footer_area_contents_->set_background(
1118 views::Background::CreateVerticalGradientBackground(
1119 kFooterTopColor,
1120 kFooterBottomColor));
1121
1122 views::View* footer_area = new views::View;
1123 footer_area->SetLayoutManager(new views::FillLayout);
1124 // Initialize the footer area with the place holder (i.e. show nothing).
1125 footer_area->AddChildView(footer_area_place_holder_.get());
1126 return footer_area;
1127 }
1128
SelectCandidateAt(int index_in_page)1129 void CandidateWindowView::SelectCandidateAt(int index_in_page) {
1130 const int current_page_index = ComputePageIndex(lookup_table_);
1131 if (current_page_index < 0) {
1132 LOG(ERROR) << "Invalid lookup_table: " << lookup_table_.ToString();
1133 return;
1134 }
1135
1136 const int cursor_absolute_index =
1137 lookup_table_.page_size * current_page_index + index_in_page;
1138 // Ignore click on out of range views.
1139 if (cursor_absolute_index < 0 ||
1140 cursor_absolute_index >=
1141 static_cast<int>(lookup_table_.candidates.size())) {
1142 return;
1143 }
1144
1145 // Unselect the currently selected candidate.
1146 candidate_views_[selected_candidate_index_in_page_]->Unselect();
1147 // Remember the currently selected candidate index in the current page.
1148 selected_candidate_index_in_page_ = index_in_page;
1149
1150 // Select the candidate specified by index_in_page.
1151 candidate_views_[index_in_page]->Select();
1152
1153 // Update the cursor indexes in the model.
1154 lookup_table_.cursor_absolute_index = cursor_absolute_index;
1155 }
1156
OnCandidateDragged(const gfx::Point & location)1157 void CandidateWindowView::OnCandidateDragged(
1158 const gfx::Point& location) {
1159 for (size_t i = 0; i < candidate_views_.size(); ++i) {
1160 gfx::Point converted_location = location;
1161 views::View::ConvertPointToView(this, candidate_views_[i],
1162 &converted_location);
1163 if (candidate_views_[i]->HitTest(converted_location)) {
1164 SelectCandidateAt(i);
1165 break;
1166 }
1167 }
1168 }
1169
CommitCandidate()1170 void CandidateWindowView::CommitCandidate() {
1171 // For now, we don't distinguish left and right clicks.
1172 const int button = 1; // Left button.
1173 const int key_modifilers = 0;
1174 FOR_EACH_OBSERVER(Observer, observers_,
1175 OnCandidateCommitted(selected_candidate_index_in_page_,
1176 button,
1177 key_modifilers));
1178 }
1179
ResizeAndMoveParentFrame()1180 void CandidateWindowView::ResizeAndMoveParentFrame() {
1181 ResizeParentFrame();
1182 MoveParentFrame();
1183 }
1184
ResizeParentFrame()1185 void CandidateWindowView::ResizeParentFrame() {
1186 // Resize the parent frame, with the current candidate window size.
1187 gfx::Size size = GetPreferredSize();
1188 gfx::Rect bounds = parent_frame_->GetClientAreaScreenBounds();
1189 // SetBounds() is not cheap. Only call this when the size is changed.
1190 if (bounds.size() != size) {
1191 bounds.set_size(size);
1192 parent_frame_->SetBounds(bounds);
1193 }
1194 }
1195
MoveParentFrame()1196 void CandidateWindowView::MoveParentFrame() {
1197 const int x = cursor_location_.x();
1198 const int y = cursor_location_.y();
1199 const int height = cursor_location_.height();
1200 const int horizontal_offset = GetHorizontalOffset();
1201
1202 gfx::Rect frame_bounds = parent_frame_->GetClientAreaScreenBounds();
1203 gfx::Rect screen_bounds = views::Screen::GetMonitorWorkAreaNearestWindow(
1204 parent_frame_->GetNativeView());
1205
1206 // The default position.
1207 frame_bounds.set_x(x + horizontal_offset);
1208 frame_bounds.set_y(y + height);
1209
1210 // Handle overflow at the left and the top.
1211 frame_bounds.set_x(std::max(frame_bounds.x(), screen_bounds.x()));
1212 frame_bounds.set_y(std::max(frame_bounds.y(), screen_bounds.y()));
1213
1214 // Handle overflow at the right.
1215 const int right_overflow = frame_bounds.right() - screen_bounds.right();
1216 if (right_overflow > 0) {
1217 frame_bounds.set_x(frame_bounds.x() - right_overflow);
1218 }
1219
1220 // Handle overflow at the bottom.
1221 const int bottom_overflow = frame_bounds.bottom() - screen_bounds.bottom();
1222 if (bottom_overflow > 0) {
1223 frame_bounds.set_y(frame_bounds.y() - height - frame_bounds.height());
1224 }
1225
1226 // Move the window per the cursor location.
1227 parent_frame_->SetBounds(frame_bounds);
1228 }
1229
GetHorizontalOffset()1230 int CandidateWindowView::GetHorizontalOffset() {
1231 // Compute the horizontal offset if the lookup table is vertical.
1232 if (!candidate_views_.empty() &&
1233 lookup_table_.orientation == InputMethodLookupTable::kVertical) {
1234 return - candidate_views_[0]->GetCandidateLabelPosition().x();
1235 }
1236 return 0;
1237 }
1238
VisibilityChanged(View * starting_from,bool is_visible)1239 void CandidateWindowView::VisibilityChanged(View* starting_from,
1240 bool is_visible) {
1241 if (is_visible) {
1242 // If the visibility of candidate window is changed,
1243 // we should move the frame to the right position.
1244 MoveParentFrame();
1245 }
1246 }
1247
OnBoundsChanged(const gfx::Rect & previous_bounds)1248 void CandidateWindowView::OnBoundsChanged(const gfx::Rect& previous_bounds) {
1249 // If the bounds(size) of candidate window is changed,
1250 // we should move the frame to the right position.
1251 View::OnBoundsChanged(previous_bounds);
1252 MoveParentFrame();
1253 }
1254
Init()1255 bool CandidateWindowController::Impl::Init() {
1256 // Initialize the input method UI status connection.
1257 InputMethodUiStatusMonitorFunctions functions;
1258 functions.hide_auxiliary_text =
1259 &CandidateWindowController::Impl::OnHideAuxiliaryText;
1260 functions.hide_lookup_table =
1261 &CandidateWindowController::Impl::OnHideLookupTable;
1262 functions.set_cursor_location =
1263 &CandidateWindowController::Impl::OnSetCursorLocation;
1264 functions.update_auxiliary_text =
1265 &CandidateWindowController::Impl::OnUpdateAuxiliaryText;
1266 functions.update_lookup_table =
1267 &CandidateWindowController::Impl::OnUpdateLookupTable;
1268 ui_status_connection_ = MonitorInputMethodUiStatus(functions, this);
1269 if (!ui_status_connection_) {
1270 LOG(ERROR) << "MonitorInputMethodUiStatus() failed.";
1271 return false;
1272 }
1273 MonitorInputMethodConnection(
1274 ui_status_connection_,
1275 &CandidateWindowController::Impl::OnConnectionChange);
1276
1277 // Create the candidate window view.
1278 CreateView();
1279
1280 return true;
1281 }
1282
CreateView()1283 void CandidateWindowController::Impl::CreateView() {
1284 // Create a non-decorated frame.
1285 frame_.reset(views::Widget::CreateWidget(
1286 views::Widget::CreateParams(views::Widget::CreateParams::TYPE_POPUP)));
1287 // The size is initially zero.
1288 frame_->Init(NULL, gfx::Rect(0, 0));
1289
1290 // Create the candidate window.
1291 candidate_window_ = new CandidateWindowView(frame_.get());
1292 candidate_window_->Init();
1293 candidate_window_->AddObserver(this);
1294
1295 // Put the candidate window view on the frame. The frame is resized
1296 // later when the candidate window is shown.
1297 views::RootView* root_view = frame_->GetRootView();
1298 // |root_view| owns the |candidate_window_|, thus |frame_| effectively
1299 // owns |candidate_window_|.
1300 root_view->SetContentsView(candidate_window_);
1301 }
1302
Impl()1303 CandidateWindowController::Impl::Impl()
1304 : ui_status_connection_(NULL),
1305 frame_(NULL) {
1306 }
1307
~Impl()1308 CandidateWindowController::Impl::~Impl() {
1309 candidate_window_->RemoveObserver(this);
1310 chromeos::DisconnectInputMethodUiStatus(ui_status_connection_);
1311 }
1312
OnHideAuxiliaryText(void * input_method_library)1313 void CandidateWindowController::Impl::OnHideAuxiliaryText(
1314 void* input_method_library) {
1315 CandidateWindowController::Impl* controller =
1316 static_cast<CandidateWindowController::Impl*>(input_method_library);
1317
1318 controller->candidate_window_->HideAuxiliaryText();
1319 controller->candidate_window_->ResizeParentFrame();
1320 }
1321
OnHideLookupTable(void * input_method_library)1322 void CandidateWindowController::Impl::OnHideLookupTable(
1323 void* input_method_library) {
1324 CandidateWindowController::Impl* controller =
1325 static_cast<CandidateWindowController::Impl*>(input_method_library);
1326
1327 controller->candidate_window_->HideLookupTable();
1328 }
1329
OnSetCursorLocation(void * input_method_library,int x,int y,int width,int height)1330 void CandidateWindowController::Impl::OnSetCursorLocation(
1331 void* input_method_library,
1332 int x,
1333 int y,
1334 int width,
1335 int height) {
1336 CandidateWindowController::Impl* controller =
1337 static_cast<CandidateWindowController::Impl*>(input_method_library);
1338
1339 // A workaround for http://crosbug.com/6460. We should ignore very short Y
1340 // move to prevent the window from shaking up and down.
1341 const int kKeepPositionThreshold = 2; // px
1342 const gfx::Rect& last_location =
1343 controller->candidate_window_->cursor_location();
1344 const int delta_y = abs(last_location.y() - y);
1345 if ((last_location.x() == x) && (delta_y <= kKeepPositionThreshold)) {
1346 DLOG(INFO) << "Ignored set_cursor_location signal to prevent window shake";
1347 return;
1348 }
1349
1350 // Remember the cursor location.
1351 controller->candidate_window_->set_cursor_location(
1352 gfx::Rect(x, y, width, height));
1353 // Move the window per the cursor location.
1354 controller->candidate_window_->MoveParentFrame();
1355 }
1356
OnUpdateAuxiliaryText(void * input_method_library,const std::string & utf8_text,bool visible)1357 void CandidateWindowController::Impl::OnUpdateAuxiliaryText(
1358 void* input_method_library,
1359 const std::string& utf8_text,
1360 bool visible) {
1361 CandidateWindowController::Impl* controller =
1362 static_cast<CandidateWindowController::Impl*>(input_method_library);
1363 // If it's not visible, hide the auxiliary text and return.
1364 if (!visible) {
1365 controller->candidate_window_->HideAuxiliaryText();
1366 return;
1367 }
1368 controller->candidate_window_->UpdateAuxiliaryText(utf8_text);
1369 controller->candidate_window_->ShowAuxiliaryText();
1370 controller->candidate_window_->ResizeParentFrame();
1371 }
1372
OnUpdateLookupTable(void * input_method_library,const InputMethodLookupTable & lookup_table)1373 void CandidateWindowController::Impl::OnUpdateLookupTable(
1374 void* input_method_library,
1375 const InputMethodLookupTable& lookup_table) {
1376 CandidateWindowController::Impl* controller =
1377 static_cast<CandidateWindowController::Impl*>(input_method_library);
1378
1379 // If it's not visible, hide the window and return.
1380 if (!lookup_table.visible) {
1381 controller->candidate_window_->HideLookupTable();
1382 return;
1383 }
1384
1385 controller->candidate_window_->UpdateCandidates(lookup_table);
1386 controller->candidate_window_->ResizeParentFrame();
1387 controller->frame_->Show();
1388 }
1389
OnCandidateCommitted(int index,int button,int flags)1390 void CandidateWindowController::Impl::OnCandidateCommitted(int index,
1391 int button,
1392 int flags) {
1393 NotifyCandidateClicked(ui_status_connection_, index, button, flags);
1394 }
1395
OnConnectionChange(void * input_method_library,bool connected)1396 void CandidateWindowController::Impl::OnConnectionChange(
1397 void* input_method_library,
1398 bool connected) {
1399 if (!connected) {
1400 CandidateWindowController::Impl* controller =
1401 static_cast<CandidateWindowController::Impl*>(input_method_library);
1402 controller->candidate_window_->HideLookupTable();
1403 }
1404 }
1405
CandidateWindowController()1406 CandidateWindowController::CandidateWindowController()
1407 : impl_(new CandidateWindowController::Impl) {
1408 }
1409
~CandidateWindowController()1410 CandidateWindowController::~CandidateWindowController() {
1411 delete impl_;
1412 }
1413
Init()1414 bool CandidateWindowController::Init() {
1415 return impl_->Init();
1416 }
1417
1418 } // namespace chromeos
1419