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