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 <set>
31
32#import "renderer/mac/CandidateView.h"
33
34#include "base/logging.h"
35#include "base/mutex.h"
36#include "client/client_interface.h"
37#include "protocol/commands.pb.h"
38#include "protocol/renderer_style.pb.h"
39#include "renderer/mac/mac_view_util.h"
40#include "renderer/table_layout.h"
41#include "renderer/renderer_style_handler.h"
42
43
44using mozc::client::SendCommandInterface;
45using mozc::commands::Candidates;
46using mozc::commands::Output;
47using mozc::commands::SessionCommand;
48using mozc::renderer::TableLayout;
49using mozc::renderer::RendererStyle;
50using mozc::renderer::RendererStyleHandler;
51using mozc::renderer::mac::MacViewUtil;
52using mozc::once_t;
53using mozc::CallOnce;
54
55// Those constants and most rendering logic is as same as Windows
56// native candidate window.
57// TODO(mukai): integrate and share the code among Win and Mac.
58
59namespace {
60const NSImage *g_LogoImage = nullptr;
61int g_column_minimum_width = 0;
62once_t g_OnceForInitializeStyle = MOZC_ONCE_INIT;
63
64void InitializeDefaultStyle() {
65  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
66
67  RendererStyle style;
68  RendererStyleHandler::GetRendererStyle(&style);
69
70  string logo_file_name = style.logo_file_name();
71  g_LogoImage =
72    [NSImage imageNamed:[NSString stringWithUTF8String:logo_file_name.c_str()]];
73  if (g_LogoImage) {
74    // setFlipped is deprecated at Snow Leopard, but we use this because
75    // it works well with Snow Leopard and new method to deal with
76    // flipped view doesn't work with Leopard.
77    [g_LogoImage setFlipped:YES];
78
79    // Fix the image size.  Sometimes the size can be smaller than the
80    // actual size because of blank margin.
81    NSArray *logoReps = [g_LogoImage representations];
82    if (logoReps && [logoReps count] > 0) {
83      NSImageRep *representation = [logoReps objectAtIndex:0];
84      [g_LogoImage setSize:NSMakeSize([representation pixelsWide],
85                                      [representation pixelsHigh])];
86    }
87  }
88
89  NSString *nsstr =
90    [NSString stringWithUTF8String:style.column_minimum_width_string().c_str()];
91  NSDictionary *attr =
92    [NSDictionary dictionaryWithObject:[NSFont messageFontOfSize:14]
93                                forKey:NSFontAttributeName];
94  NSAttributedString *defaultMessage =
95    [[[NSAttributedString alloc] initWithString:nsstr attributes:attr]
96     autorelease];
97  g_column_minimum_width = [defaultMessage size].width;
98
99  // default line width is specified as 1.0 *pt*, but we want to draw
100  // it as 1.0 px.
101  [NSBezierPath setDefaultLineWidth:1.0];
102  [NSBezierPath setDefaultLineJoinStyle:NSMiterLineJoinStyle];
103  [pool drain];
104}
105}
106
107// Private method declarations.
108@interface CandidateView ()
109// Draw the |row|-th row.
110- (void)drawRow:(int)row;
111
112// Draw footer
113- (void)drawFooter;
114
115// Draw scroll bar
116- (void)drawVScrollBar;
117@end
118
119@implementation CandidateView
120#pragma mark initialization
121
122- (id)initWithFrame:(NSRect)frame {
123  CallOnce(&g_OnceForInitializeStyle, InitializeDefaultStyle);
124  self = [super initWithFrame:frame];
125  if (self) {
126    tableLayout_ = new(std::nothrow)TableLayout;
127    RendererStyle *style = new(std::nothrow)RendererStyle;
128    if (style) {
129      RendererStyleHandler::GetRendererStyle(style);
130    }
131    style_ = style;
132    focusedRow_ = -1;
133  }
134  if (!tableLayout_ || !style_) {
135    [self release];
136    self = nil;
137  }
138  return self;
139}
140
141- (void)setCandidates:(const Candidates *)candidates {
142  candidates_.CopyFrom(*candidates);
143}
144
145- (void)setSendCommandInterface:(SendCommandInterface *)command_sender {
146  command_sender_ = command_sender;
147}
148
149- (BOOL)isFlipped {
150  return YES;
151}
152
153- (void)dealloc {
154  [candidateStringsCache_ release];
155  delete tableLayout_;
156  delete style_;
157  [super dealloc];
158}
159
160- (const TableLayout *)tableLayout {
161  return tableLayout_;
162}
163
164#pragma mark drawing
165
166#define max(x, y)  (((x) > (y))? (x) : (y))
167- (NSSize)updateLayout {
168  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
169  [candidateStringsCache_ release];
170  tableLayout_->Initialize(candidates_.candidate_size(), NUMBER_OF_COLUMNS);
171  tableLayout_->SetWindowBorder(style_->window_border());
172
173  // calculating focusedRow_
174  if (candidates_.has_focused_index() && candidates_.candidate_size() > 0) {
175    const int focusedIndex = candidates_.focused_index();
176    focusedRow_ = focusedIndex - candidates_.candidate(0).index();
177  } else {
178    focusedRow_ = -1;
179  }
180
181  // Reserve footer space.
182  if (candidates_.has_footer()) {
183    NSSize footerSize = NSZeroSize;
184
185    const mozc::commands::Footer &footer = candidates_.footer();
186
187    if (footer.has_label()) {
188      NSAttributedString *footerLabel = MacViewUtil::ToNSAttributedString(
189          footer.label(), style_->footer_style());
190      NSSize footerLabelSize =
191          MacViewUtil::applyTheme([footerLabel size], style_->footer_style());
192      footerSize.width += footerLabelSize.width;
193      footerSize.height = max(footerSize.height, footerLabelSize.height);
194    }
195
196    if (footer.has_sub_label()) {
197      NSAttributedString *footerSubLabel = MacViewUtil::ToNSAttributedString(
198          footer.sub_label(), style_->footer_sub_label_style());
199      NSSize footerSubLabelSize =
200          MacViewUtil::applyTheme([footerSubLabel size], style_->footer_sub_label_style());
201      footerSize.width += footerSubLabelSize.width;
202      footerSize.height = max(footerSize.height, footerSubLabelSize.height);
203    }
204
205    if (footer.logo_visible() && g_LogoImage) {
206      NSSize logoSize = [g_LogoImage size];
207      footerSize.width += logoSize.width;
208      footerSize.height = max(footerSize.height, logoSize.height);
209    }
210
211    if (footer.index_visible()) {
212      const int focusedIndex = candidates_.focused_index();
213      const int totalItems = candidates_.size();
214      NSString *footerIndex =
215          [NSString stringWithFormat:@"%d/%d", focusedIndex + 1, totalItems];
216      NSAttributedString *footerAttributedIndex =
217          MacViewUtil::ToNSAttributedString([footerIndex UTF8String],
218                                            style_->footer_style());
219      NSSize footerIndexSize =
220          MacViewUtil::applyTheme([footerAttributedIndex size],
221                                  style_->footer_style());
222      footerSize.width += footerIndexSize.width;
223      footerSize.height = max(footerSize.height, footerIndexSize.height);
224    }
225
226    footerSize.height += style_->footer_border_colors_size();
227    tableLayout_->EnsureFooterSize(MacViewUtil::ToSize(footerSize));
228  }
229
230  tableLayout_->SetRowRectPadding(style_->row_rect_padding());
231  if (candidates_.candidate_size() < candidates_.size()) {
232    tableLayout_->SetVScrollBar(style_->scrollbar_width());
233  }
234
235  NSAttributedString *gap1 = MacViewUtil::ToNSAttributedString(
236      " ", style_->text_styles(COLUMN_GAP1));
237  tableLayout_->EnsureCellSize(COLUMN_GAP1, MacViewUtil::ToSize([gap1 size]));
238
239  NSMutableArray *newCache = [[NSMutableArray array] retain];
240  for (size_t i = 0; i < candidates_.candidate_size(); ++i) {
241    const Candidates::Candidate &candidate = candidates_.candidate(i);
242    NSAttributedString *shortcut = MacViewUtil::ToNSAttributedString(
243       candidate.annotation().shortcut(),
244       style_->text_styles(COLUMN_SHORTCUT));
245    string value = candidate.value();
246    if (candidate.annotation().has_prefix()) {
247      value = candidate.annotation().prefix() + value;
248    }
249    if (candidate.annotation().has_suffix()) {
250      value.append(candidate.annotation().suffix());
251    }
252    if (!value.empty()) {
253      value.append("  ");
254    }
255
256    NSAttributedString *candidateValue = MacViewUtil::ToNSAttributedString(
257        value, style_->text_styles(COLUMN_CANDIDATE));
258    NSAttributedString *description = MacViewUtil::ToNSAttributedString(
259       candidate.annotation().description(),
260       style_->text_styles(COLUMN_DESCRIPTION));
261    if ([shortcut length] > 0) {
262      NSSize shortcutSize = MacViewUtil::applyTheme(
263          [shortcut size], style_->text_styles(COLUMN_SHORTCUT));
264      tableLayout_->EnsureCellSize(COLUMN_SHORTCUT,
265                                   MacViewUtil::ToSize(shortcutSize));
266    }
267    if ([candidateValue length] > 0) {
268      NSSize valueSize = MacViewUtil::applyTheme(
269          [candidateValue size], style_->text_styles(COLUMN_CANDIDATE));
270      tableLayout_->EnsureCellSize(COLUMN_CANDIDATE,
271                                   MacViewUtil::ToSize(valueSize));
272    }
273    if ([description length] > 0) {
274      NSSize descriptionSize = MacViewUtil::applyTheme(
275          [description size], style_->text_styles(COLUMN_DESCRIPTION));
276      tableLayout_->EnsureCellSize(COLUMN_DESCRIPTION,
277                                   MacViewUtil::ToSize(descriptionSize));
278    }
279
280    [newCache addObject:[NSArray arrayWithObjects:shortcut, gap1,
281                                 candidateValue, description, nil]];
282  }
283
284  tableLayout_->EnsureColumnsWidth(COLUMN_CANDIDATE, COLUMN_DESCRIPTION,
285                                   g_column_minimum_width);
286
287  candidateStringsCache_ = newCache;
288  tableLayout_->FreezeLayout();
289  [pool drain];
290  return MacViewUtil::ToNSSize(tableLayout_->GetTotalSize());
291}
292
293- (void)drawRect:(NSRect)rect {
294  if (!Category_IsValid(candidates_.category())) {
295    LOG(WARNING) << "Unknown candidates category: " << candidates_.category();
296    return;
297  }
298
299  for (int i = 0; i < candidates_.candidate_size(); ++i) {
300    [self drawRow:i];
301  }
302
303  if (candidates_.candidate_size() < candidates_.size()) {
304    [self drawVScrollBar];
305  }
306  [self drawFooter];
307
308  // Draw the window border at last
309  [MacViewUtil::ToNSColor(style_->border_color()) set];
310  mozc::Size windowSize = tableLayout_->GetTotalSize();
311  [NSBezierPath strokeRect:NSMakeRect(
312      0.5, 0.5, windowSize.width - 1, windowSize.height - 1)];
313}
314
315#pragma mark drawing aux methods
316
317- (void)drawRow:(int)row {
318  if (row == focusedRow_) {
319    // Draw focused background
320    NSRect focusedRect = MacViewUtil::ToNSRect(tableLayout_->GetRowRect(focusedRow_));
321    [MacViewUtil::ToNSColor(style_->focused_background_color()) set];
322    [NSBezierPath fillRect:focusedRect];
323    [MacViewUtil::ToNSColor(style_->focused_border_color()) set];
324    // Fix the border position.  Because a line should be drawn at the
325    // middle point of the pixel, origin should be shifted by 0.5 unit
326    // and the size should be shrinked by 1.0 unit.
327    focusedRect.origin.x += 0.5;
328    focusedRect.origin.y += 0.5;
329    focusedRect.size.width -= 1.0;
330    focusedRect.size.height -= 1.0;
331    [NSBezierPath strokeRect:focusedRect];
332  } else {
333    // Draw normal background
334    for (int i = COLUMN_SHORTCUT; i < NUMBER_OF_COLUMNS; ++i) {
335      mozc::Rect cellRect = tableLayout_->GetCellRect(row, i);
336      if (cellRect.size.width > 0 && cellRect.size.height > 0 &&
337          style_->text_styles(i).has_background_color()) {
338        [MacViewUtil::ToNSColor(style_->text_styles(i).background_color()) set];
339        [NSBezierPath fillRect:MacViewUtil::ToNSRect(cellRect)];
340      }
341    }
342  }
343
344  NSArray *candidate = [candidateStringsCache_ objectAtIndex:row];
345  for (int i = COLUMN_SHORTCUT; i < NUMBER_OF_COLUMNS; ++i) {
346    NSAttributedString *text = [candidate objectAtIndex:i];
347    NSRect cellRect = MacViewUtil::ToNSRect(tableLayout_->GetCellRect(row, i));
348    NSPoint &candidatePosition = cellRect.origin;
349    // Adjust the positions
350    candidatePosition.x += style_->text_styles(i).left_padding();
351    candidatePosition.y += (cellRect.size.height - [text size].height) / 2;
352    [text drawAtPoint:candidatePosition];
353  }
354
355  if (candidates_.candidate(row).has_information_id()) {
356      NSRect rect = MacViewUtil::ToNSRect(tableLayout_->GetRowRect(row));
357      [MacViewUtil::ToNSColor(style_->focused_border_color()) set];
358      rect.origin.x += rect.size.width - 6.0;
359      rect.size.width = 4.0;
360      rect.origin.y += 2.0;
361      rect.size.height -= 4.0;
362      [NSBezierPath fillRect:rect];
363  }
364}
365
366- (void)drawFooter {
367  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
368  if (candidates_.has_footer()) {
369    const mozc::commands::Footer &footer = candidates_.footer();
370    NSRect footerRect = MacViewUtil::ToNSRect(tableLayout_->GetFooterRect());
371
372    // Draw footer border
373    for (int i = 0; i < style_->footer_border_colors_size(); ++i) {
374      [MacViewUtil::ToNSColor(style_->footer_border_colors(i)) set];
375      NSPoint fromPoint = NSMakePoint(footerRect.origin.x,
376                                      footerRect.origin.y + 0.5);
377      NSPoint toPoint = NSMakePoint(footerRect.origin.x + footerRect.size.width,
378                                    footerRect.origin.y + 0.5);
379      [NSBezierPath strokeLineFromPoint:fromPoint toPoint:toPoint];
380      footerRect.origin.y += 1;
381    }
382
383    // Draw Footer background and data if necessary
384    NSGradient *footerBackground =
385      [[[NSGradient alloc]
386        initWithStartingColor:MacViewUtil::ToNSColor(style_->footer_top_color())
387        endingColor:MacViewUtil::ToNSColor(style_->footer_bottom_color())]
388       autorelease];
389    [footerBackground drawInRect:footerRect angle:90.0];
390
391    // Draw logo
392    if (footer.logo_visible() && g_LogoImage) {
393      [g_LogoImage drawAtPoint:footerRect.origin
394                      fromRect:NSZeroRect /* means draw entire image */
395                     operation:NSCompositeSourceOver
396                      fraction:1.0 /* opacity */];
397      NSSize logoSize = [g_LogoImage size];
398      footerRect.origin.x += logoSize.width;
399      footerRect.size.width -= logoSize.width;
400    }
401
402    // Draw label
403    if (footer.has_label()) {
404      NSAttributedString *footerLabel = MacViewUtil::ToNSAttributedString(
405          footer.label(), style_->footer_style());
406      footerRect.origin.x += style_->footer_style().left_padding();
407      NSSize labelSize = [footerLabel size];
408      NSPoint labelPosition = footerRect.origin;
409      labelPosition.y += (footerRect.size.height - labelSize.height) / 2;
410      [footerLabel drawAtPoint:labelPosition];
411    }
412
413    // Draw sub_label
414    if (footer.has_sub_label()) {
415      NSAttributedString *footerSubLabel = MacViewUtil::ToNSAttributedString(
416          footer.sub_label(), style_->footer_sub_label_style());
417      footerRect.origin.x += style_->footer_sub_label_style().left_padding();
418      NSSize subLabelSize = [footerSubLabel size];
419      NSPoint subLabelPosition = footerRect.origin;
420      subLabelPosition.y += (footerRect.size.height - subLabelSize.height) / 2;
421      [footerSubLabel drawAtPoint:subLabelPosition];
422    }
423
424    // Draw footer index (e.g. "10/120")
425    if (footer.index_visible()) {
426      int focusedIndex = candidates_.focused_index();
427      int totalItems = candidates_.size();
428      NSString *footerIndex =
429          [NSString stringWithFormat:@"%d/%d", focusedIndex + 1, totalItems];
430      NSAttributedString *footerAttributedIndex =
431        MacViewUtil::ToNSAttributedString(
432          [footerIndex UTF8String], style_->footer_style());
433      NSSize footerSize = [footerAttributedIndex size];
434      NSPoint footerPosition = footerRect.origin;
435      footerPosition.x = footerPosition.x + footerRect.size.width -
436          footerSize.width - style_->footer_style().right_padding();
437      [footerAttributedIndex drawAtPoint:footerPosition];
438    }
439  }
440  [pool drain];
441}
442
443- (void)drawVScrollBar {
444  const mozc::Rect &vscrollRect = tableLayout_->GetVScrollBarRect();
445
446  if (!vscrollRect.IsRectEmpty() && candidates_.candidate_size() > 0) {
447    const int beginIndex = candidates_.candidate(0).index();
448    const int candidatesTotal = candidates_.size();
449    const int endIndex =
450        candidates_.candidate(candidates_.candidate_size() - 1).index();
451
452    [MacViewUtil::ToNSColor(style_->scrollbar_background_color()) set];
453    [NSBezierPath fillRect:MacViewUtil::ToNSRect(vscrollRect)];
454
455    const mozc::Rect &indicatorRect =
456        tableLayout_->GetVScrollIndicatorRect(
457            beginIndex, endIndex, candidatesTotal);
458    [MacViewUtil::ToNSColor(style_->scrollbar_indicator_color()) set];
459    [NSBezierPath fillRect:MacViewUtil::ToNSRect(indicatorRect)];
460  }
461}
462
463#pragma mark event handling callbacks
464
465const char *Inspect(id obj) {
466  return [[NSString stringWithFormat:@"%@", obj] UTF8String];
467}
468
469- (void)mouseDown:(NSEvent *)event {
470  mozc::Point localPos = MacViewUtil::ToPoint(
471      [self convertPoint:[event locationInWindow] fromView:nil]);
472  int clickedRow = -1;
473  for (int i = 0; i < tableLayout_->number_of_rows(); ++i) {
474    mozc::Rect rowRect = tableLayout_->GetRowRect(i);
475    if (rowRect.PtrInRect(localPos)) {
476      clickedRow = i;
477      break;
478    }
479  }
480
481  if (clickedRow >= 0 && clickedRow != focusedRow_) {
482    focusedRow_ = clickedRow;
483    [self setNeedsDisplay:YES];
484  }
485}
486
487- (void)mouseUp:(NSEvent *)event {
488  mozc::Point localPos = MacViewUtil::ToPoint(
489      [self convertPoint:[event locationInWindow] fromView:nil]);
490  if (command_sender_ == nullptr) {
491    return;
492  }
493  if (candidates_.candidate_size() < tableLayout_->number_of_rows()) {
494    return;
495  }
496  for (int i = 0; i < tableLayout_->number_of_rows(); ++i) {
497    mozc::Rect rowRect = tableLayout_->GetRowRect(i);
498    if (rowRect.PtrInRect(localPos)) {
499      SessionCommand command;
500      command.set_type(SessionCommand::SELECT_CANDIDATE);
501      command.set_id(candidates_.candidate(i).id());
502      Output dummy_output;
503      command_sender_->SendCommand(command, &dummy_output);
504      break;
505    }
506  }
507}
508
509- (void)mouseDragged:(NSEvent *)event {
510  [self mouseDown:event];
511}
512@end
513