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