1 // Copyright 2010-2018, Google Inc.
2 // All rights reserved.
3 //
4 // Redistribution and use in source and binary forms, with or without
5 // modification, are permitted provided that the following conditions are
6 // met:
7 //
8 //     * Redistributions of source code must retain the above copyright
9 // notice, this list of conditions and the following disclaimer.
10 //     * Redistributions in binary form must reproduce the above
11 // copyright notice, this list of conditions and the following disclaimer
12 // in the documentation and/or other materials provided with the
13 // distribution.
14 //     * Neither the name of Google Inc. nor the names of its
15 // contributors may be used to endorse or promote products derived from
16 // this software without specific prior written permission.
17 //
18 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 
30 #include "renderer/unix/candidate_window.h"
31 
32 #include <sstream>
33 
34 #include "base/logging.h"
35 #include "base/util.h"
36 #include "client/client_interface.h"
37 #include "renderer/renderer_style_handler.h"
38 #include "renderer/table_layout.h"
39 #include "renderer/unix/cairo_factory_interface.h"
40 #include "renderer/unix/const.h"
41 #include "renderer/unix/draw_tool.h"
42 #include "renderer/unix/font_spec.h"
43 #include "renderer/unix/text_renderer.h"
44 
45 namespace mozc {
46 namespace renderer {
47 namespace gtk {
48 
49 namespace {
GetIndexGuideString(const commands::Candidates & candidates)50 string GetIndexGuideString(const commands::Candidates &candidates) {
51   if (!candidates.has_footer() || !candidates.footer().index_visible()) {
52     return "";
53   }
54 
55   const int focused_index = candidates.focused_index();
56   const int total_items = candidates.size();
57 
58   std::stringstream footer_string;
59   return Util::StringPrintf("%d/%d ", focused_index + 1, total_items);
60 }
61 
GetCandidateArrayIndexByCandidateIndex(const commands::Candidates & candidates,int candidate_index)62 int GetCandidateArrayIndexByCandidateIndex(
63     const commands::Candidates &candidates,
64     int candidate_index) {
65   for (size_t i = 0; i < candidates.candidate_size(); ++i) {
66     const commands::Candidates::Candidate &candidate =
67         candidates.candidate(i);
68     if (candidate.index() == candidate_index) {
69       return i;
70     }
71   }
72   return candidates.candidate_size();
73 }
74 }  // namespace
75 
CandidateWindow(TableLayoutInterface * table_layout,TextRendererInterface * text_renderer,DrawToolInterface * draw_tool,GtkWrapperInterface * gtk,CairoFactoryInterface * cairo_factory)76 CandidateWindow::CandidateWindow(
77     TableLayoutInterface *table_layout,
78     TextRendererInterface *text_renderer,
79     DrawToolInterface *draw_tool,
80     GtkWrapperInterface *gtk,
81     CairoFactoryInterface *cairo_factory)
82     : GtkWindowBase(gtk),
83       table_layout_(table_layout),
84       text_renderer_(text_renderer),
85       draw_tool_(draw_tool),
86       cairo_factory_(cairo_factory) {
87 }
88 
OnPaint(GtkWidget * widget,GdkEventExpose * event)89 bool CandidateWindow::OnPaint(GtkWidget *widget, GdkEventExpose* event) {
90   draw_tool_->Reset(cairo_factory_->CreateCairoInstance(
91       GetCanvasWidget()->window));
92 
93   DrawBackground();
94   DrawShortcutBackground();
95   DrawSelectedRect();
96   DrawCells();
97   DrawInformationIcon();
98   DrawVScrollBar();
99   DrawFooter();
100   DrawFrame();
101   return true;
102 }
103 
DrawBackground()104 void CandidateWindow::DrawBackground() {
105   const Rect window_rect(Point(0, 0), GetWindowSize());
106   draw_tool_->FillRect(window_rect, kDefaultBackgroundColor);
107 }
108 
DrawShortcutBackground()109 void CandidateWindow::DrawShortcutBackground() {
110   if (table_layout_->number_of_columns() <= 0) {
111     return;
112   }
113 
114   const Rect first_column_rect = table_layout_->GetColumnRect(0);
115   const Rect first_row_rect = table_layout_->GetRowRect(0);
116   if (first_column_rect.IsRectEmpty() || first_row_rect.IsRectEmpty()) {
117     return;
118   }
119 
120   const Rect shortcut_background_area(first_row_rect.origin,
121                                       first_column_rect.size);
122   draw_tool_->FillRect(shortcut_background_area, kShortcutBackgroundColor);
123 }
124 
DrawSelectedRect()125 void CandidateWindow::DrawSelectedRect() {
126   if (!candidates_.has_focused_index()) {
127     return;
128   }
129 
130   const int selected_row_index = GetCandidateArrayIndexByCandidateIndex(
131       candidates_, candidates_.focused_index());
132 
133   DCHECK_GE(selected_row_index, 0);
134 
135   if (selected_row_index >= candidates_.candidate_size()) {
136     LOG(ERROR) << "focused index is invalid" << candidates_.focused_index();
137     return;
138   }
139 
140   const Rect selected_rect = table_layout_->GetRowRect(selected_row_index);
141   draw_tool_->FillRect(selected_rect, kSelectedRowBackgroundColor);
142   draw_tool_->FrameRect(selected_rect, kSelectedRowFrameColor, 1.0);
143 }
144 
DrawCells()145 void CandidateWindow::DrawCells() {
146   for (size_t i = 0; i < candidates_.candidate_size(); ++i) {
147     const commands::Candidates::Candidate &candidate
148         = candidates_.candidate(i);
149     string shortcut, value, description;
150 
151     GetDisplayString(candidate, &shortcut, &value, &description);
152 
153     if (!shortcut.empty()) {
154       text_renderer_->RenderText(shortcut,
155                                  table_layout_->GetCellRect(i, COLUMN_SHORTCUT),
156                                  FontSpec::FONTSET_SHORTCUT);
157     }
158 
159     if (!value.empty()) {
160       text_renderer_->RenderText(
161           value,
162           table_layout_->GetCellRect(i, COLUMN_CANDIDATE),
163           FontSpec::FONTSET_CANDIDATE);
164     }
165 
166     if (!description.empty()) {
167       text_renderer_->RenderText(
168           description,
169           table_layout_->GetCellRect(i, COLUMN_DESCRIPTION),
170           FontSpec::FONTSET_DESCRIPTION);
171     }
172   }
173 }
174 
DrawInformationIcon()175 void CandidateWindow::DrawInformationIcon() {
176   for (size_t i = 0; i < candidates_.candidate_size(); ++i) {
177     if (!candidates_.candidate(i).has_information_id()) {
178       continue;
179     }
180     const Rect row_rect = table_layout_->GetRowRect(i);
181     const Rect usage_information_indicator_rect(
182         row_rect.origin.x + row_rect.size.width - 6,
183         row_rect.origin.y + 2,
184         4,
185         row_rect.size.height - 4);
186 
187     draw_tool_->FillRect(usage_information_indicator_rect, kIndicatorColor);
188     draw_tool_->FrameRect(usage_information_indicator_rect, kIndicatorColor, 1);
189   }
190 }
191 
DrawVScrollBar()192 void CandidateWindow::DrawVScrollBar() {
193   // TODO(nona): implement this function
194 }
195 
DrawFooterSeparator(Rect * footer_content_area)196 void CandidateWindow::DrawFooterSeparator(Rect *footer_content_area) {
197   DCHECK(footer_content_area);
198   const Point dest(footer_content_area->Right(), footer_content_area->Top());
199   draw_tool_->DrawLine(footer_content_area->origin, dest, kFrameColor,
200                        kFooterSeparatorHeight);
201   // The remaining footer content area is the one after removal of above/below
202   // separation line.
203   footer_content_area->origin.y += kFooterSeparatorHeight;
204   footer_content_area->size.height -= kFooterSeparatorHeight;
205 }
206 
DrawFooterIndex(Rect * footer_content_rect)207 void CandidateWindow::DrawFooterIndex(Rect *footer_content_rect) {
208   DCHECK(footer_content_rect);
209   if (!candidates_.has_footer() ||
210       !candidates_.footer().index_visible() ||
211       !candidates_.has_focused_index()) {
212     return;
213   }
214 
215   const string index_guide_string = GetIndexGuideString(candidates_);
216   const Size index_guide_size = text_renderer_->GetPixelSize(
217       FontSpec::FONTSET_FOOTER_INDEX, index_guide_string);
218   // Render as right-aligned.
219   Rect index_rect(footer_content_rect->Right() - index_guide_size.width,
220                   footer_content_rect->Top(),
221                   index_guide_size.width,
222                   footer_content_rect->Height());
223   text_renderer_->RenderText(index_guide_string,
224                              index_rect,
225                              FontSpec::FONTSET_FOOTER_INDEX);
226   footer_content_rect->size.width -= index_guide_size.width;
227 }
228 
DrawFooterLabel(const Rect & footer_content_rect)229 void CandidateWindow::DrawFooterLabel(const Rect &footer_content_rect) {
230   if (footer_content_rect.IsRectEmpty()) {
231     return;
232   }
233   if (candidates_.footer().has_label()) {
234     text_renderer_->RenderText(candidates_.footer().label(),
235                                footer_content_rect,
236                                FontSpec::FONTSET_FOOTER_LABEL);
237   } else if (candidates_.footer().has_sub_label()) {
238     text_renderer_->RenderText(candidates_.footer().sub_label(),
239                                footer_content_rect,
240                                FontSpec::FONTSET_FOOTER_SUBLABEL);
241   }
242 }
243 
DrawLogo(Rect * footer_content_rect)244 void CandidateWindow::DrawLogo(Rect *footer_content_rect) {
245   DCHECK(footer_content_rect);
246   // TODO(nona): Implement this.
247   // The current implementation is just a padding area.
248   if (candidates_.footer().logo_visible()) {
249     // The 47 pixel is same as icon width to be rendered in future.
250     footer_content_rect->size.width -= 47;
251     footer_content_rect->origin.x += 47;
252   }
253 }
254 
DrawFooter()255 void CandidateWindow::DrawFooter() {
256   if (!candidates_.has_footer()) {
257     return;
258   }
259 
260   Rect footer_content_area = table_layout_->GetFooterRect();
261   if (footer_content_area.IsRectEmpty()) {
262     return;
263   }
264 
265   DrawFooterSeparator(&footer_content_area);
266   DrawLogo(&footer_content_area);
267   DrawFooterIndex(&footer_content_area);
268   DrawFooterLabel(footer_content_area);
269 }
270 
DrawFrame()271 void CandidateWindow::DrawFrame() {
272   const Rect client_rect(Point(0, 0), table_layout_->GetTotalSize());
273   draw_tool_->FrameRect(client_rect, kFrameColor, 1);
274 }
275 
Initialize()276 void CandidateWindow::Initialize() {
277   text_renderer_->Initialize(GetCanvasWidget()->window);
278 }
279 
UpdateScrollBarSize()280 void CandidateWindow::UpdateScrollBarSize() {
281   // TODO(nona) : Implement this.
282 }
283 
UpdateFooterSize()284 void CandidateWindow::UpdateFooterSize() {
285   if (!candidates_.has_footer()) {
286     return;
287   }
288 
289   Size footer_size(0, 0);
290 
291   if (candidates_.footer().has_label()) {
292     const Size label_string_size = text_renderer_->GetPixelSize(
293         FontSpec::FONTSET_FOOTER_LABEL,
294         candidates_.footer().label());
295     footer_size.width += label_string_size.width;
296     footer_size.height = std::max(footer_size.height, label_string_size.height);
297   } else if (candidates_.footer().has_sub_label()) {
298     const Size label_string_size = text_renderer_->GetPixelSize(
299         FontSpec::FONTSET_FOOTER_LABEL,
300         candidates_.footer().sub_label());
301     footer_size.width += label_string_size.width;
302     footer_size.height = std::max(footer_size.height, label_string_size.height);
303   }
304 
305   if (candidates_.footer().index_visible()) {
306     const Size index_guide_size = text_renderer_->GetPixelSize(
307       FontSpec::FONTSET_FOOTER_INDEX,
308       GetIndexGuideString(candidates_));
309     footer_size.width += index_guide_size.width;
310     footer_size.height = std::max(footer_size.height, index_guide_size.height);
311   }
312 
313   if (candidates_.candidate_size() < candidates_.size()) {
314     const Size minimum_size = text_renderer_->GetPixelSize(
315       FontSpec::FONTSET_CANDIDATE,
316       kMinimumCandidateAndDescriptionWidthAsString);
317     table_layout_->EnsureColumnsWidth(
318       COLUMN_CANDIDATE, COLUMN_DESCRIPTION, minimum_size.width);
319   }
320 
321   if (candidates_.footer().logo_visible()) {
322     // TODO(nona): Implement logo sizing.
323     footer_size.width += 47;
324   }
325   footer_size.height += kFooterSeparatorHeight;
326 
327   table_layout_->EnsureFooterSize(footer_size);
328 }
329 
UpdateGap1Size()330 void CandidateWindow::UpdateGap1Size() {
331   const Size gap1_size =
332       text_renderer_->GetPixelSize(FontSpec::FONTSET_CANDIDATE, " ");
333   table_layout_->EnsureCellSize(COLUMN_GAP1, gap1_size);
334 }
335 
UpdateCandidatesSize(bool * has_description)336 void CandidateWindow::UpdateCandidatesSize(bool *has_description) {
337   DCHECK(has_description);
338   *has_description = false;
339   for (size_t i = 0; i < candidates_.candidate_size(); ++i) {
340     const commands::Candidates::Candidate &candidate =
341         candidates_.candidate(i);
342 
343     string shortcut, description, candidate_string;
344     GetDisplayString(candidate, &shortcut, &candidate_string, &description);
345 
346     if (!shortcut.empty()) {
347       string text;
348       text.push_back(' ');
349       text.append(shortcut);
350       text.push_back(' ');
351       const Size rendering_size = text_renderer_->GetPixelSize(
352           FontSpec::FONTSET_SHORTCUT, text);
353       table_layout_->EnsureCellSize(COLUMN_SHORTCUT, rendering_size);
354     }
355 
356     if (!candidate_string.empty()) {
357       const Size rendering_size = text_renderer_->GetPixelSize(
358           FontSpec::FONTSET_CANDIDATE, candidate_string);
359       table_layout_->EnsureCellSize(COLUMN_CANDIDATE, rendering_size);
360     }
361 
362     if (!description.empty()) {
363       string text;
364       text.append(description);
365       text.push_back(' ');
366       const Size rendering_size = text_renderer_->GetPixelSize(
367           FontSpec::FONTSET_DESCRIPTION, text);
368       table_layout_->EnsureCellSize(COLUMN_DESCRIPTION, rendering_size);
369       *has_description = true;
370     }
371   }
372 }
373 
UpdateGap2Size(bool has_description)374 void CandidateWindow::UpdateGap2Size(bool has_description) {
375   const char *gap2_string = (has_description ? "   " : " ");
376   const Size gap2_size = text_renderer_->GetPixelSize(
377       FontSpec::FONTSET_CANDIDATE, gap2_string);
378   table_layout_->EnsureCellSize(COLUMN_GAP2, gap2_size);
379 }
380 
Update(const commands::Candidates & candidates)381 Size CandidateWindow::Update(const commands::Candidates &candidates) {
382   DCHECK(
383       (candidates_.category()  == commands::CONVERSION) ||
384       (candidates_.category()  == commands::PREDICTION) ||
385       (candidates_.category()  == commands::TRANSLITERATION) ||
386       (candidates_.category()  == commands::SUGGESTION) ||
387       (candidates_.category()  == commands::USAGE))
388       << "Unknown candidate category" << candidates_.category();
389 
390   candidates_.CopyFrom(candidates);
391 
392   table_layout_->Initialize(candidates_.candidate_size(), NUMBER_OF_COLUMNS);
393   table_layout_->SetWindowBorder(kWindowBorder);
394   table_layout_->SetRowRectPadding(kRowRectPadding);
395 
396   UpdateScrollBarSize();
397   UpdateFooterSize();
398   UpdateGap1Size();
399   bool has_description;
400   UpdateCandidatesSize(&has_description);
401   UpdateGap2Size(has_description);
402 
403   table_layout_->FreezeLayout();
404   Resize(table_layout_->GetTotalSize());
405   Redraw();
406   return table_layout_->GetTotalSize();
407 }
408 
GetDisplayString(const commands::Candidates::Candidate & candidate,string * shortcut,string * value,string * description)409 void CandidateWindow::GetDisplayString(
410     const commands::Candidates::Candidate &candidate,
411     string *shortcut,
412     string *value,
413     string *description) {
414   DCHECK(shortcut);
415   DCHECK(value);
416   DCHECK(description);
417 
418   shortcut->clear();
419   value->clear();
420   description->clear();
421 
422   if (!candidate.has_value()) {
423     return;
424   }
425   value->assign(candidate.value());
426 
427   if (!candidate.has_annotation()) {
428     return;
429   }
430 
431   const commands::Annotation &annotation = candidate.annotation();
432 
433   if (annotation.has_shortcut()) {
434     shortcut->assign(annotation.shortcut());
435   }
436 
437   if (annotation.has_description()) {
438     description->assign(annotation.description());
439   }
440 
441   if (annotation.has_prefix()) {
442     value->assign(annotation.prefix());
443     value->append(candidate.value());
444   }
445 
446   if (annotation.has_suffix()) {
447     value->append(annotation.suffix());
448   }
449 }
450 
GetCandidateColumnInClientCord() const451 Rect CandidateWindow::GetCandidateColumnInClientCord() const {
452   DCHECK(table_layout_->IsLayoutFrozen()) << "Table layout is not frozen.";
453   return table_layout_->GetCellRect(0, COLUMN_CANDIDATE);
454 }
455 
OnMouseLeftUp(const Point & pos)456 void CandidateWindow::OnMouseLeftUp(const Point &pos) {
457   if (send_command_interface_ == NULL) {
458     LOG(ERROR) << "send_command_interface_ is NULL";
459     return;
460   }
461 
462   const int kSelectedIdx = GetSelectedRowIndex(pos);
463   if (kSelectedIdx == -1) {  // out of range
464     return;
465   }
466 
467   const commands::Candidates::Candidate &candidate =
468       candidates_.candidate(kSelectedIdx);
469   commands::SessionCommand command;
470   command.set_type(commands::SessionCommand::SELECT_CANDIDATE);
471   command.set_id(candidate.id());
472   commands::Output output;
473   send_command_interface_->SendCommand(command, &output);
474   return;
475 }
476 
GetSelectedRowIndex(const Point & pos) const477 int CandidateWindow::GetSelectedRowIndex(const Point &pos) const {
478   for (size_t i = 0; i < candidates_.candidate_size(); ++i) {
479     const Rect rect = table_layout_->GetRowRect(i);
480 
481     if (rect.PtrInRect(pos)) {
482       return i;
483     }
484   }
485   return -1;
486 }
487 
SetSendCommandInterface(client::SendCommandInterface * send_command_interface)488 bool CandidateWindow::SetSendCommandInterface(
489     client::SendCommandInterface *send_command_interface) {
490   send_command_interface_ = send_command_interface;
491   return true;
492 }
493 
ReloadFontConfig(const string & font_description)494 void CandidateWindow::ReloadFontConfig(const string &font_description) {
495   text_renderer_->ReloadFontConfig(font_description);
496 }
497 
498 }  // namespace gtk
499 }  // namespace renderer
500 }  // namespace mozc
501