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#import "mac/GoogleJapaneseInputController.h" 31 32#import <Carbon/Carbon.h> 33#import <Cocoa/Cocoa.h> 34#import <InputMethodKit/IMKInputController.h> 35#import <InputMethodKit/IMKServer.h> 36 37#include <unistd.h> 38 39#include <cstdlib> 40#include <set> 41 42#import "mac/GoogleJapaneseInputControllerInterface.h" 43#import "mac/GoogleJapaneseInputServer.h" 44#import "mac/KeyCodeMap.h" 45 46#include "base/const.h" 47#include "base/logging.h" 48#include "base/mac_process.h" 49#include "base/mac_util.h" 50#include "base/mutex.h" 51#include "base/process.h" 52#include "base/util.h" 53#include "client/client.h" 54#include "ipc/ipc.h" 55#include "protocol/commands.pb.h" 56#include "protocol/config.pb.h" 57#include "renderer/renderer_client.h" 58#include "session/ime_switch_util.h" 59 60using mozc::commands::Candidates; 61using mozc::commands::Capability; 62using mozc::commands::CompositionMode; 63using mozc::commands::Input; 64using mozc::commands::KeyEvent; 65using mozc::commands::Output; 66using mozc::commands::Preedit; 67using mozc::commands::RendererCommand; 68using mozc::commands::SessionCommand; 69using mozc::config::Config; 70using mozc::config::ImeSwitchUtil; 71using mozc::kProductNameInEnglish; 72using mozc::once_t; 73using mozc::CallOnce; 74using mozc::MacProcess; 75 76namespace { 77// set of bundle IDs of applications on which Mozc should not open urls. 78const std::set<string> *gNoOpenLinkApps = nullptr; 79// The mapping from the CompositionMode enum to the actual id string 80// of composition modes. 81const std::map<CompositionMode, NSString *> *gModeIdMap = nullptr; 82const std::set<string> *gNoSelectedRangeApps = nullptr; 83const std::set<string> *gNoDisplayModeSwitchApps = nullptr; 84const std::set<string> *gNoSurroundingTextApps = nullptr; 85 86// TODO(horo): This value should be get from system configuration. 87// DoubleClickInterval can be get from NSEvent (MacOSX ver >= 10.6) 88const NSTimeInterval kDoubleTapInterval = 0.5; 89 90const int kMaxSurroundingLength = 20; 91// In some apllications when the client's text length is large, getting the 92// surrounding text takes too much time. So we set this limitation. 93const int kGetSurroundingTextClientLengthLimit = 1000; 94 95NSString *GetLabelForSuffix(const string &suffix) { 96 string label = mozc::MacUtil::GetLabelForSuffix(suffix); 97 return [[NSString stringWithUTF8String:label.c_str()] retain]; 98} 99 100CompositionMode GetCompositionMode(NSString *modeID) { 101 if (modeID == nullptr) { 102 LOG(ERROR) << "modeID could not be initialized."; 103 return mozc::commands::DIRECT; 104 } 105 106 // The name of direct input mode. This name is determined at 107 // Info.plist. We don't use com.google... instead of 108 // com.apple... because of a hack for Java Swing applications like 109 // JEdit. If we use our own IDs for those modes, such applications 110 // work incorrectly for some reasons. 111 // 112 // The document for ID names is available at: 113 // http://developer.apple.com/legacy/mac/library/documentation/Carbon/ 114 // Reference/Text_Services_Manager/Reference/reference.html 115 if ([modeID isEqual:@"com.apple.inputmethod.Roman"]) { 116 // TODO(komatsu): This should be mozc::commands::HALF_ASCII, when 117 // we can handle the difference between the direct mode and the 118 // half ascii mode. 119 DLOG(INFO) << "com.apple.inputmethod.Roman"; 120 return mozc::commands::HALF_ASCII; 121 } 122 123 if ([modeID isEqual:@"com.apple.inputmethod.Japanese.Katakana"]) { 124 DLOG(INFO) << "com.apple.inputmethod.Japanese.Katakana"; 125 return mozc::commands::FULL_KATAKANA; 126 } 127 128 if ([modeID isEqual:@"com.apple.inputmethod.Japanese.HalfWidthKana"]) { 129 DLOG(INFO) << "com.apple.inputmethod.Japanese.HalfWidthKana"; 130 return mozc::commands::HALF_KATAKANA; 131 } 132 133 if ([modeID isEqual:@"com.apple.inputmethod.Japanese.FullWidthRoman"]) { 134 DLOG(INFO) << "com.apple.inputmethod.Japanese.FullWidthRoman"; 135 return mozc::commands::FULL_ASCII; 136 } 137 138 if ([modeID isEqual:@"com.apple.inputmethod.Japanese"]) { 139 DLOG(INFO) << "com.apple.inputmethod.Japanese"; 140 return mozc::commands::HIRAGANA; 141 } 142 143 LOG(ERROR) << "The code should not reach here."; 144 return mozc::commands::DIRECT; 145} 146 147bool IsBannedApplication(const std::set<string>* bundleIdSet, 148 const string& bundleId) { 149 return bundleIdSet == nullptr || bundleId.empty() || 150 bundleIdSet->find(bundleId) != bundleIdSet->end(); 151} 152} // namespace 153 154 155@implementation GoogleJapaneseInputController 156#pragma mark accessors for testing 157@synthesize keyCodeMap = keyCodeMap_; 158@synthesize yenSignCharacter = yenSignCharacter_; 159@synthesize mode = mode_; 160@synthesize rendererCommand = rendererCommand_; 161@synthesize replacementRange = replacementRange_; 162@synthesize imkClientForTest = imkClientForTest_; 163- (mozc::client::ClientInterface *)mozcClient { 164 return mozcClient_; 165} 166- (void)setMozcClient:(mozc::client::ClientInterface *)newMozcClient { 167 delete mozcClient_; 168 mozcClient_ = newMozcClient; 169} 170- (mozc::renderer::RendererInterface *)renderer { 171 return candidateController_; 172} 173- (void)setRenderer:(mozc::renderer::RendererInterface *)newRenderer { 174 delete candidateController_; 175 candidateController_ = newRenderer; 176} 177 178 179#pragma mark object init/dealloc 180// Initializer designated in IMKInputController. see: 181// http://developer.apple.com/documentation/Cocoa/Reference/IMKInputController_Class/ 182 183- (id)initWithServer:(IMKServer *)server 184 delegate:(id)delegate 185 client:(id)inputClient { 186 self = [super initWithServer:server delegate:delegate client:inputClient]; 187 if (!self) { 188 return self; 189 } 190 keyCodeMap_ = [[KeyCodeMap alloc] init]; 191 clientBundle_ = new(std::nothrow) string; 192 replacementRange_ = NSMakeRange(NSNotFound, 0); 193 originalString_ = [[NSMutableString alloc] init]; 194 composedString_ = [[NSMutableAttributedString alloc] init]; 195 cursorPosition_ = -1; 196 mode_ = mozc::commands::DIRECT; 197 checkInputMode_ = YES; 198 suppressSuggestion_ = NO; 199 yenSignCharacter_ = mozc::config::Config::YEN_SIGN; 200 candidateController_ = new(std::nothrow) mozc::renderer::RendererClient; 201 rendererCommand_ = new(std::nothrow)RendererCommand; 202 mozcClient_ = mozc::client::ClientFactory::NewClient(); 203 imkServer_ = reinterpret_cast<id<ServerCallback> >(server); 204 imkClientForTest_ = nil; 205 lastKeyDownTime_ = 0; 206 lastKeyCode_ = 0; 207 208 // We don't check the return value of NSBundle because it fails during tests. 209 [NSBundle loadNibNamed:@"Config" owner:self]; 210 if (!originalString_ || !composedString_ || !candidateController_ || 211 !rendererCommand_ || !mozcClient_ || !clientBundle_) { 212 [self release]; 213 self = nil; 214 } else { 215 DLOG(INFO) << [[NSString stringWithFormat:@"initWithServer: %@ %@ %@", 216 server, delegate, inputClient] UTF8String]; 217 if (!candidateController_->Activate()) { 218 LOG(ERROR) << "Cannot activate renderer"; 219 delete candidateController_; 220 candidateController_ = nullptr; 221 } 222 [self setupClientBundle:inputClient]; 223 [self setupCapability]; 224 RendererCommand::ApplicationInfo *applicationInfo = 225 rendererCommand_->mutable_application_info(); 226 applicationInfo->set_process_id(::getpid()); 227 // thread_id and receiver_handle are not used currently in Mac but 228 // set some values to prevent warning. 229 applicationInfo->set_thread_id(0); 230 applicationInfo->set_receiver_handle(0); 231 } 232 233 return self; 234} 235 236- (void)dealloc { 237 [keyCodeMap_ release]; 238 [originalString_ release]; 239 [composedString_ release]; 240 [imkClientForTest_ release]; 241 delete clientBundle_; 242 delete candidateController_; 243 delete mozcClient_; 244 delete rendererCommand_; 245 DLOG(INFO) << "dealloc server"; 246 [super dealloc]; 247} 248 249- (id)client { 250 if (imkClientForTest_) { 251 return imkClientForTest_; 252 } 253 return [super client]; 254} 255 256- (NSMenu*)menu { 257 return menu_; 258} 259 260+ (void)initializeConstants { 261 std::set<string> *noOpenlinkApps = new(std::nothrow) std::set<string>; 262 if (noOpenlinkApps) { 263 // should not open links during screensaver. 264 noOpenlinkApps->insert("com.apple.securityagent"); 265 gNoOpenLinkApps = noOpenlinkApps; 266 } 267 268 std::map<CompositionMode, NSString *> *newMap = 269 new(std::nothrow) std::map<CompositionMode, NSString *>; 270 if (newMap) { 271 (*newMap)[mozc::commands::DIRECT] = GetLabelForSuffix("Roman"); 272 (*newMap)[mozc::commands::HIRAGANA] = GetLabelForSuffix("base"); 273 (*newMap)[mozc::commands::FULL_KATAKANA] = GetLabelForSuffix("Katakana"); 274 (*newMap)[mozc::commands::HALF_ASCII] = GetLabelForSuffix("Roman"); 275 (*newMap)[mozc::commands::FULL_ASCII] = GetLabelForSuffix("FullWidthRoman"); 276 (*newMap)[mozc::commands::HALF_KATAKANA] = 277 GetLabelForSuffix("FullWidthRoman"); 278 gModeIdMap = newMap; 279 } 280 281 std::set<string> *noSelectedRangeApps = new(std::nothrow) std::set<string>; 282 if (noSelectedRangeApps) { 283 // Do not call selectedRange: method for the following 284 // applications because it could lead to application crash. 285 noSelectedRangeApps->insert("com.microsoft.Excel"); 286 noSelectedRangeApps->insert("com.microsoft.Powerpoint"); 287 noSelectedRangeApps->insert("com.microsoft.Word"); 288 gNoSelectedRangeApps = noSelectedRangeApps; 289 } 290 291 // Do not call selectInputMode: method for the following 292 // applications because it could cause some unexpected behavior. 293 // MS-Word: When the display mode goes to ASCII but there is no 294 // compositions, it goes to direct input mode instead of Half-ASCII 295 // mode. When the first composition character is alphanumeric (such 296 // like pressing Shift-A at first), that character is directly 297 // inserted into application instead of composition starting "A". 298 std::set<string> *noDisplayModeSwitchApps = 299 new(std::nothrow) std::set<string>; 300 if (noDisplayModeSwitchApps) { 301 noDisplayModeSwitchApps->insert("com.microsoft.Word"); 302 gNoDisplayModeSwitchApps = noDisplayModeSwitchApps; 303 } 304 305 std::set<string> *noSurroundingTextApps = new(std::nothrow) std::set<string>; 306 if (noSurroundingTextApps) { 307 // Disables the surrounding text feature for the following application 308 // because calling attributedSubstringFromRange to it is very heavy. 309 noSurroundingTextApps->insert("com.evernote.Evernote"); 310 gNoSurroundingTextApps = noSurroundingTextApps; 311 } 312} 313 314#pragma mark IMKStateSetting Protocol 315// Currently it just ignores the following methods: 316// Modes, showPreferences, valueForTag 317// They are described at 318// http://developer.apple.com/documentation/Cocoa/Reference/IMKStateSetting_Protocol/ 319 320- (void)activateServer:(id)sender { 321 [super activateServer:sender]; 322 checkInputMode_ = YES; 323 if (rendererCommand_->visible() && candidateController_) { 324 candidateController_->ExecCommand(*rendererCommand_); 325 } 326 [self handleConfig]; 327 [imkServer_ setCurrentController:self]; 328 329 string window_name, window_owner; 330 if (mozc::MacUtil::GetFrontmostWindowNameAndOwner(&window_name, 331 &window_owner)) { 332 DLOG(INFO) << "frontmost window name: \"" << window_name << "\" " 333 << "owner: \"" << window_owner << "\""; 334 if (mozc::MacUtil::IsSuppressSuggestionWindow(window_name, window_owner)) { 335 suppressSuggestion_ = YES; 336 } else { 337 suppressSuggestion_ = NO; 338 } 339 } 340 341 DLOG(INFO) << kProductNameInEnglish << " client (" << self 342 << "): activated for " << sender; 343 DLOG(INFO) << "sender bundleID: " << *clientBundle_; 344} 345 346- (void)deactivateServer:(id)sender { 347 RendererCommand clearCommand; 348 clearCommand.set_type(RendererCommand::UPDATE); 349 clearCommand.set_visible(false); 350 clearCommand.clear_output(); 351 if (candidateController_) { 352 candidateController_->ExecCommand(clearCommand); 353 } 354 DLOG(INFO) << kProductNameInEnglish << " client (" << self 355 << "): deactivated"; 356 DLOG(INFO) << "sender bundleID: " << *clientBundle_; 357 [super deactivateServer:sender]; 358} 359 360- (NSUInteger)recognizedEvents:(id)sender { 361 // Because we want to handle single Shift key pressing later, now I 362 // turned on NSFlagsChanged also. 363 return NSKeyDownMask | NSFlagsChangedMask; 364} 365 366// This method is called when a user changes the input mode. 367- (void)setValue:(id)value forTag:(long)tag client:(id)sender { 368 CompositionMode new_mode = GetCompositionMode(value); 369 370 if (new_mode == mozc::commands::HALF_ASCII && [composedString_ length] == 0) { 371 new_mode = mozc::commands::DIRECT; 372 } 373 374 [self switchMode:new_mode client:sender]; 375 [self handleConfig]; 376 [super setValue:value forTag:tag client:sender]; 377} 378 379 380#pragma mark internal methods 381 382- (void)handleConfig { 383 // Get the config and set client-side behaviors 384 Config config; 385 if (!mozcClient_->GetConfig(&config)) { 386 LOG(ERROR) << "Cannot obtain the current config"; 387 return; 388 } 389 390 InputMode input_mode = ASCII; 391 if (config.preedit_method() == Config::KANA) { 392 input_mode = KANA; 393 } 394 [keyCodeMap_ setInputMode:input_mode]; 395 yenSignCharacter_ = config.yen_sign_character(); 396 397 if (config.use_japanese_layout()) { 398 // Apple does not have "Japanese" layout actually -- here sets 399 // "US" layout, which means US-ASCII layout or JIS layout 400 // depending on which type of keyboard is actually connected. 401 [[self client] overrideKeyboardWithKeyboardNamed:@"com.apple.keylayout.US"]; 402 } 403} 404 405- (void)setupClientBundle:(id)sender { 406 NSString *bundleIdentifier = [sender bundleIdentifier]; 407 if (bundleIdentifier != nil && [bundleIdentifier length] > 0) { 408 clientBundle_->assign([bundleIdentifier UTF8String]); 409 } 410} 411 412- (void)setupCapability { 413 Capability capability; 414 415 if (IsBannedApplication(gNoSelectedRangeApps, *clientBundle_)) { 416 capability.set_text_deletion(Capability::NO_TEXT_DELETION_CAPABILITY); 417 } else { 418 capability.set_text_deletion(Capability::DELETE_PRECEDING_TEXT); 419 } 420 421 mozcClient_->set_client_capability(capability); 422} 423 424// Mode changes to direct and clean up the status. 425- (void)switchModeToDirect:(id)sender { 426 mode_ = mozc::commands::DIRECT; 427 DLOG(INFO) << "Mode switch: HIRAGANA, KATAKANA, etc. -> DIRECT"; 428 KeyEvent keyEvent; 429 Output output; 430 keyEvent.set_special_key(mozc::commands::KeyEvent::OFF); 431 mozcClient_->SendKey(keyEvent, &output); 432 if (output.has_result()) { 433 [self commitText:output.result().value().c_str() client:sender]; 434 } 435 if ([composedString_ length] > 0) { 436 [self updateComposedString:nullptr]; 437 [self clearCandidates]; 438 } 439} 440 441// change the mode to the new mode and turn-on the IME if necessary. 442- (void)switchModeInternal:(CompositionMode)new_mode { 443 if (mode_ == mozc::commands::DIRECT) { 444 // Input mode changes from direct to an active mode. 445 DLOG(INFO) << "Mode switch: DIRECT -> HIRAGANA, KATAKANA, etc."; 446 KeyEvent keyEvent; 447 Output output; 448 keyEvent.set_special_key(mozc::commands::KeyEvent::ON); 449 mozcClient_->SendKey(keyEvent, &output); 450 } 451 452 if (mode_ != new_mode) { 453 // Switch input mode. 454 DLOG(INFO) << "Switch input mode."; 455 SessionCommand command; 456 command.set_type(mozc::commands::SessionCommand::SWITCH_INPUT_MODE); 457 command.set_composition_mode(new_mode); 458 Output output; 459 mozcClient_->SendCommand(command, &output); 460 mode_ = new_mode; 461 } 462} 463 464- (void)switchMode:(CompositionMode)new_mode client:(id)sender { 465 if (mode_ == new_mode) { 466 return; 467 } 468 if (mode_ != mozc::commands::DIRECT && new_mode == mozc::commands::DIRECT) { 469 [self switchModeToDirect:sender]; 470 } else if (new_mode != mozc::commands::DIRECT) { 471 [self switchModeInternal:new_mode]; 472 } 473} 474 475- (void)switchDisplayMode { 476 if (gModeIdMap == nullptr) { 477 LOG(ERROR) << "gModeIdMap is not initialized correctly."; 478 return; 479 } 480 if (IsBannedApplication(gNoDisplayModeSwitchApps, *clientBundle_)) { 481 return; 482 } 483 484 std::map<CompositionMode, NSString *>::const_iterator it = 485 gModeIdMap->find(mode_); 486 if (it == gModeIdMap->end()) { 487 LOG(ERROR) << "mode: " << mode_ << " is invalid"; 488 return; 489 } 490 491 [[self client] selectInputMode:it->second]; 492} 493 494- (void)commitText:(const char *)text client:(id)sender { 495 if (text == nullptr) { 496 return; 497 } 498 499 [sender insertText:[NSString stringWithUTF8String:text] 500 replacementRange:replacementRange_]; 501 replacementRange_ = NSMakeRange(NSNotFound, 0); 502} 503 504- (void)launchWordRegisterTool:(id)client { 505 ::setenv(mozc::kWordRegisterEnvironmentName, "", 1); 506 if (!IsBannedApplication(gNoSelectedRangeApps, *clientBundle_)) { 507 NSRange selectedRange = [client selectedRange]; 508 if (selectedRange.location != NSNotFound && 509 selectedRange.length != NSNotFound && 510 selectedRange.length > 0) { 511 NSString *text = 512 [[client attributedSubstringFromRange:selectedRange] string]; 513 if (text != nil) { 514 :: setenv(mozc::kWordRegisterEnvironmentName, [text UTF8String], 1); 515 } 516 } 517 } 518 MacProcess::LaunchMozcTool("word_register_dialog"); 519} 520 521- (void)invokeReconvert:(const SessionCommand *)command client:(id)sender { 522 if (IsBannedApplication(gNoSelectedRangeApps, *clientBundle_)) { 523 return; 524 } 525 526 NSRange selectedRange = [sender selectedRange]; 527 if (selectedRange.location == NSNotFound || 528 selectedRange.length == NSNotFound) { 529 // the application does not support reconversion. 530 return; 531 } 532 533 DLOG(INFO) << selectedRange.location << ", " << selectedRange.length; 534 SessionCommand sending_command; 535 Output output; 536 sending_command.CopyFrom(*command); 537 538 if (selectedRange.length == 0) { 539 // Currently no range is selected for reconversion. Tries to 540 // invoke UNDO instead. 541 [self invokeUndo:sender]; 542 return; 543 } 544 545 if (!sending_command.has_text()) { 546 NSString *text = [[sender attributedSubstringFromRange:selectedRange] string]; 547 if (!text) { 548 return; 549 } 550 sending_command.set_text([text UTF8String]); 551 } 552 553 if (mozcClient_->SendCommand(sending_command, &output)) { 554 replacementRange_ = selectedRange; 555 [self processOutput:&output client:sender]; 556 } 557} 558 559- (void)invokeUndo:(id)sender { 560 if (IsBannedApplication(gNoSelectedRangeApps, *clientBundle_)) { 561 return; 562 } 563 564 NSRange selectedRange = [sender selectedRange]; 565 if (selectedRange.location == NSNotFound || 566 selectedRange.length == NSNotFound || 567 // Some applications such like iTunes does not return NSNotFound 568 // range but (0, 0). However, the range starting with negative 569 // location has to be invalid, then we can reject such apps. 570 selectedRange.location == 0) { 571 return; 572 } 573 574 DLOG(INFO) << selectedRange.location << ", " << selectedRange.length; 575 SessionCommand command; 576 Output output; 577 command.set_type(SessionCommand::UNDO); 578 if (mozcClient_->SendCommand(command, &output)) { 579 [self processOutput:&output client:sender]; 580 } 581} 582 583- (void)processOutput:(const mozc::commands::Output *)output client:(id)sender { 584 if (output == nullptr) { 585 return; 586 } 587 if (!output->consumed()) { 588 return; 589 } 590 591 DLOG(INFO) << output->DebugString(); 592 if (output->has_url()) { 593 NSString *url = [NSString stringWithUTF8String:output->url().c_str()]; 594 [self openLink:[NSURL URLWithString:url]]; 595 } 596 597 if (output->has_result()) { 598 [self commitText:output->result().value().c_str() client:sender]; 599 } 600 601 // Handles deletion range. We do not even handle it for some 602 // applications to prevent application crashes. 603 if (output->has_deletion_range() && 604 !IsBannedApplication(gNoSelectedRangeApps, *clientBundle_)) { 605 if ([composedString_ length] == 0 && 606 replacementRange_.location == NSNotFound) { 607 NSRange selectedRange = [sender selectedRange]; 608 const mozc::commands::DeletionRange &deletion_range = 609 output->deletion_range(); 610 if (selectedRange.location != NSNotFound || 611 selectedRange.length != NSNotFound || 612 selectedRange.location + deletion_range.offset() > 0) { 613 // The offset is a negative value. See protocol/commands.proto for 614 // the details. 615 selectedRange.location += deletion_range.offset(); 616 selectedRange.length += deletion_range.length(); 617 replacementRange_ = selectedRange; 618 } 619 } else { 620 // We have to consider the case that there is already 621 // composition and/or we already set the position of the 622 // composition by replacementRange_. We do nothing here at this 623 // time because we already found that it will involve several 624 // buggy behaviors with Carbon apps and MS Office. 625 // TODO(mukai): find the right behavior. 626 } 627 } 628 629 [self updateComposedString:&(output->preedit())]; 630 [self updateCandidates:output]; 631 632 if (output->has_mode()) { 633 CompositionMode new_mode = output->mode(); 634 // Do not allow HALF_ASCII with empty composition. This should be 635 // handled in the converter, but just in case. 636 if (new_mode == mozc::commands::HALF_ASCII && 637 (!output->has_preedit() || output->preedit().segment_size() == 0)) { 638 new_mode = mozc::commands::DIRECT; 639 [self switchMode:new_mode client:sender]; 640 } 641 if (new_mode != mode_) { 642 mode_ = new_mode; 643 [self switchDisplayMode]; 644 } 645 } 646 647 if (output->has_launch_tool_mode()) { 648 switch (output->launch_tool_mode()) { 649 case mozc::commands::Output::CONFIG_DIALOG: 650 MacProcess::LaunchMozcTool("config_dialog"); 651 break; 652 case mozc::commands::Output::DICTIONARY_TOOL: 653 MacProcess::LaunchMozcTool("dictionary_tool"); 654 break; 655 case mozc::commands::Output::WORD_REGISTER_DIALOG: 656 [self launchWordRegisterTool:sender]; 657 break; 658 default: 659 // do nothing 660 break; 661 } 662 } 663 664 // Handle callbacks. 665 if (output->has_callback() && output->callback().has_session_command()) { 666 const SessionCommand &callback_command = 667 output->callback().session_command(); 668 if (callback_command.type() == SessionCommand::CONVERT_REVERSE) { 669 [self invokeReconvert:&callback_command client:sender]; 670 } else if (callback_command.type() == SessionCommand::UNDO) { 671 [self invokeUndo:sender]; 672 } else { 673 Output output_for_callback; 674 if (mozcClient_->SendCommand(callback_command, &output_for_callback)) { 675 [self processOutput:&output_for_callback client:sender]; 676 } 677 } 678 } 679} 680 681#pragma mark Mozc Server methods 682 683 684#pragma mark IMKServerInput Protocol 685// Currently GoogleJapaneseInputController uses handleEvent:client: 686// method to handle key events. It does not support inputText:client: 687// nor inputText:key:modifiers:client:. 688// Because GoogleJapaneseInputController does not use IMKCandidates, 689// the following methods are not needed to implement: 690// candidates 691// 692// The meaning of these methods are described at: 693// http://developer.apple.com/documentation/Cocoa/Reference/IMKServerInput_Additions/ 694 695- (id)originalString:(id)sender { 696 return originalString_; 697} 698 699- (void)updateComposedString:(const Preedit *)preedit { 700 // If the last and the current composed string length is 0, 701 // we don't call updateComposition. 702 if (([composedString_ length] == 0) && 703 ((preedit == nullptr || preedit->segment_size() == 0))) { 704 return; 705 } 706 707 [composedString_ 708 deleteCharactersInRange:NSMakeRange(0, [composedString_ length])]; 709 cursorPosition_ = -1; 710 if (preedit != nullptr) { 711 cursorPosition_ = preedit->cursor(); 712 for (size_t i = 0; i < preedit->segment_size(); ++i) { 713 NSDictionary *highlightAttributes = 714 [self markForStyle:kTSMHiliteSelectedConvertedText 715 atRange:NSMakeRange(NSNotFound, 0)]; 716 NSDictionary *underlineAttributes = 717 [self markForStyle:kTSMHiliteConvertedText 718 atRange:NSMakeRange(NSNotFound, 0)]; 719 const Preedit::Segment& seg = preedit->segment(i); 720 NSDictionary *attr = (seg.annotation() == Preedit::Segment::HIGHLIGHT)? 721 highlightAttributes : underlineAttributes; 722 NSString *seg_string = 723 [NSString stringWithUTF8String:seg.value().c_str()]; 724 NSAttributedString *seg_attributed_string = 725 [[[NSAttributedString alloc] 726 initWithString:seg_string attributes:attr] 727 autorelease]; 728 [composedString_ appendAttributedString:seg_attributed_string]; 729 } 730 } 731 if ([composedString_ length] == 0) { 732 [originalString_ setString:@""]; 733 replacementRange_ = NSMakeRange(NSNotFound, 0); 734 } 735 736 // Make composed string visible to the client applications. 737 [self updateComposition]; 738} 739 740- (void)commitComposition:(id)sender { 741 if ([composedString_ length] == 0) { 742 DLOG(INFO) << "Nothing is committed."; 743 return; 744 } 745 [self commitText:[[composedString_ string] UTF8String] client:sender]; 746 747 SessionCommand command; 748 Output output; 749 command.set_type(SessionCommand::SUBMIT); 750 mozcClient_->SendCommand(command, &output); 751 [self clearCandidates]; 752 [self updateComposedString:nullptr]; 753} 754 755- (id)composedString:(id)sender { 756 return composedString_; 757} 758 759- (void)clearCandidates { 760 rendererCommand_->set_type(RendererCommand::UPDATE); 761 rendererCommand_->set_visible(false); 762 rendererCommand_->clear_output(); 763 if (candidateController_) { 764 candidateController_->ExecCommand(*rendererCommand_); 765 } 766} 767 768// |selecrionRange| method is defined at IMKInputController class and 769// means the position of cursor actually. 770- (NSRange)selectionRange { 771 return (cursorPosition_ == -1) ? 772 [super selectionRange] : // default behavior defined at super class 773 NSMakeRange(cursorPosition_, 0); 774} 775 776- (void)delayedUpdateCandidates { 777 // The candidate window position is not recalculated if the 778 // candidate already appears on the screen. Therefore, if a user 779 // moves client application window by mouse, candidate window won't 780 // follow the move of window. This is done because: 781 // - some applications like Emacs or Google Chrome don't return the 782 // cursor position correctly. The candidate window moves 783 // frequently with those application, which irritates users. 784 // - Kotoeri does this too. 785 if ((!rendererCommand_->visible()) && 786 (rendererCommand_->output().candidates().candidate_size() > 0)) { 787 NSRect preeditRect = NSZeroRect; 788 const int32 position = rendererCommand_->output().candidates().position(); 789 // Some applications throws error when we call attributesForCharacterIndex. 790 DLOG(INFO) << "attributesForCharacterIndex: " << position; 791 @try { 792 [[self client] attributesForCharacterIndex:position 793 lineHeightRectangle:&preeditRect]; 794 } 795 @catch (NSException *exception) { 796 LOG(ERROR) << "Exception from [" << *clientBundle_ << "] " 797 << [[exception name] UTF8String] << "," 798 << [[exception reason] UTF8String]; 799 } 800 DLOG(INFO) << " preeditRect: ((" 801 << preeditRect.origin.x << ", " 802 << preeditRect.origin.y << "), (" 803 << preeditRect.size.width << ", " 804 << preeditRect.size.height << "))"; 805 NSScreen *baseScreen = nil; 806 NSRect baseFrame = NSZeroRect; 807 for (baseScreen in [NSScreen screens]) { 808 baseFrame = [baseScreen frame]; 809 if (baseFrame.origin.x == 0 && baseFrame.origin.y == 0) { 810 break; 811 } 812 } 813 int baseHeight = baseFrame.size.height; 814 rendererCommand_->mutable_preedit_rectangle()->set_left( 815 preeditRect.origin.x); 816 rendererCommand_->mutable_preedit_rectangle()->set_top( 817 baseHeight - preeditRect.origin.y - preeditRect.size.height); 818 rendererCommand_->mutable_preedit_rectangle()->set_right( 819 preeditRect.origin.x + preeditRect.size.width); 820 rendererCommand_->mutable_preedit_rectangle()->set_bottom( 821 baseHeight - preeditRect.origin.y); 822 } 823 824 rendererCommand_->set_visible( 825 rendererCommand_->output().candidates().candidate_size() > 0); 826 if (candidateController_) { 827 candidateController_->ExecCommand(*rendererCommand_); 828 } 829} 830 831- (void)updateCandidates:(const Output *)output { 832 if (output == nullptr) { 833 [self clearCandidates]; 834 return; 835 } 836 837 rendererCommand_->set_type(RendererCommand::UPDATE); 838 rendererCommand_->mutable_output()->CopyFrom(*output); 839 840 // Runs delayedUpdateCandidates in the next event loop. 841 // This is because some applications like Google Docs with Chrome returns 842 // incorrect cursor position if we call attributesForCharacterIndex here. 843 [self performSelector:@selector(delayedUpdateCandidates) 844 withObject:nil 845 afterDelay:0]; 846} 847 848- (void)openLink:(NSURL *)url { 849 // Open a link specified by |url|. Any opening link behavior should 850 // call this method because it checks the capability of application. 851 // On some application like login window of screensaver, opening 852 // link behavior should not happen because it can cause some 853 // security issues. 854 if (!clientBundle_ || IsBannedApplication(gNoOpenLinkApps, *clientBundle_)) { 855 return; 856 } 857 [[NSWorkspace sharedWorkspace] openURL:url]; 858} 859 860- (BOOL)fillSurroundingContext:(mozc::commands::Context *)context 861 client:(id<IMKTextInput>)client { 862 NSInteger totalLength = [client length]; 863 if (totalLength == 0 || totalLength == NSNotFound || 864 totalLength > kGetSurroundingTextClientLengthLimit) { 865 return false; 866 } 867 NSRange selectedRange = [client selectedRange]; 868 if (selectedRange.location == NSNotFound || 869 selectedRange.length == NSNotFound) { 870 return false; 871 } 872 NSRange precedingRange = NSMakeRange(0, selectedRange.location); 873 if (selectedRange.location > kMaxSurroundingLength) { 874 precedingRange = 875 NSMakeRange(selectedRange.location - kMaxSurroundingLength, 876 kMaxSurroundingLength); 877 } 878 NSString *precedingString = 879 [[client attributedSubstringFromRange:precedingRange] string]; 880 if (precedingString) { 881 context->set_preceding_text([precedingString UTF8String]); 882 DLOG(INFO) << "preceding_text: \"" << context->preceding_text() << "\""; 883 } 884 return true; 885} 886 887- (BOOL)handleEvent:(NSEvent *)event client:(id)sender { 888 if ([event type] == NSCursorUpdate) { 889 [self updateComposition]; 890 return NO; 891 } 892 if ([event type] != NSKeyDown && [event type] != NSFlagsChanged) { 893 return NO; 894 } 895 896 // Handle KANA key and EISU key. We explicitly handles this here 897 // for mode switch because some text area such like iPhoto person 898 // name editor does not call setValue:forTag:client: method. 899 // see: http://www.google.com/support/forum/p/ime/thread?tid=3aafb74ff71a1a69&hl=ja&fid=3aafb74ff71a1a690004aa3383bc9f5d 900 if ([event type] == NSKeyDown) { 901 NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970]; 902 const NSTimeInterval elapsedTime = currentTime - lastKeyDownTime_; 903 const bool isDoubleTap = ([event keyCode] == lastKeyCode_) && 904 (elapsedTime < kDoubleTapInterval); 905 lastKeyDownTime_ = currentTime; 906 lastKeyCode_ = [event keyCode]; 907 908 // these calling of switchMode: can be duplicated if the 909 // application sends the setValue:forTag:client: and handleEvent: 910 // at the same key event, but that's okay because switchMode: 911 // method does nothing if the new mode is same as the current 912 // mode. 913 if ([event keyCode] == kVK_JIS_Kana) { 914 [self switchMode:mozc::commands::HIRAGANA client:sender]; 915 [self switchDisplayMode]; 916 if (isDoubleTap) { 917 SessionCommand command; 918 command.set_type(SessionCommand::CONVERT_REVERSE); 919 [self invokeReconvert:&command client:sender]; 920 } 921 } else if ([event keyCode] == kVK_JIS_Eisu) { 922 if (isDoubleTap) { 923 SessionCommand command; 924 command.set_type(SessionCommand::COMMIT_RAW_TEXT); 925 [self sendCommand:command]; 926 } 927 CompositionMode new_mode = ([composedString_ length] == 0) ? 928 mozc::commands::DIRECT : mozc::commands::HALF_ASCII; 929 [self switchMode:new_mode client:sender]; 930 [self switchDisplayMode]; 931 } 932 } 933 934 if ([keyCodeMap_ isModeSwitchingKey:event]) { 935 // Special hack for Eisu/Kana keys. Sometimes those key events 936 // come to this method but we should ignore them because some 937 // applications like PhotoShop is stuck. 938 return YES; 939 } 940 941 // Get the Mozc key event 942 KeyEvent keyEvent; 943 if (![keyCodeMap_ getMozcKeyCodeFromKeyEvent:event 944 toMozcKeyEvent:&keyEvent]) { 945 // Modifier flags change (not submitted to the server yet), or 946 // unsupported key pressed. 947 return NO; 948 } 949 950 // If the key event is turn on event, the key event has to be sent 951 // to the server anyway. 952 if (mode_ == mozc::commands::DIRECT && 953 !ImeSwitchUtil::IsDirectModeCommand(keyEvent)) { 954 // Yen sign special hack: although the current mode is DIRECT, 955 // backslash is sent instead of yen sign for JIS yen key with no 956 // modifiers. This behavior is based on the configuration. 957 if ([event keyCode] == kVK_JIS_Yen && 958 [event modifierFlags] == 0 && 959 yenSignCharacter_ == mozc::config::Config::BACKSLASH) { 960 [self commitText:"\\" client:sender]; 961 return YES; 962 } 963 return NO; 964 } 965 966 // Send the key event to the server actually 967 Output output; 968 969 if (isprint(keyEvent.key_code())) { 970 [originalString_ appendFormat:@"%c", keyEvent.key_code()]; 971 } 972 973 mozc::commands::Context context; 974 if (suppressSuggestion_) { 975 // TODO(komatsu, horo): Support Google Omnibox too. 976 context.add_experimental_features("google_search_box"); 977 } 978 keyEvent.set_mode(mode_); 979 980 if ([composedString_ length] == 0 && 981 !IsBannedApplication(gNoSelectedRangeApps, *clientBundle_) && 982 !IsBannedApplication(gNoSurroundingTextApps, *clientBundle_)) { 983 [self fillSurroundingContext:&context client:sender]; 984 } 985 if (!mozcClient_->SendKeyWithContext(keyEvent, context, &output)) { 986 return NO; 987 } 988 989 [self processOutput:&output client:sender]; 990 return output.consumed(); 991} 992 993#pragma mark callbacks 994- (void)sendCommand:(const SessionCommand &)command { 995 Output output; 996 if (!mozcClient_->SendCommand(command, &output)) { 997 return; 998 } 999 1000 [self processOutput:&output client:[self client]]; 1001} 1002 1003- (IBAction)reconversionClicked:(id)sender { 1004 SessionCommand command; 1005 command.set_type(SessionCommand::CONVERT_REVERSE); 1006 [self invokeReconvert:&command client:[self client]]; 1007} 1008 1009- (IBAction)configClicked:(id)sender { 1010 MacProcess::LaunchMozcTool("config_dialog"); 1011} 1012 1013- (IBAction)dictionaryToolClicked:(id)sender { 1014 MacProcess::LaunchMozcTool("dictionary_tool"); 1015} 1016 1017- (IBAction)registerWordClicked:(id)sender { 1018 [self launchWordRegisterTool:[self client]]; 1019} 1020 1021- (IBAction)characterPaletteClicked:(id)sender { 1022 MacProcess::LaunchMozcTool("character_palette"); 1023} 1024 1025- (IBAction)handWritingClicked:(id)sender { 1026 MacProcess::LaunchMozcTool("hand_writing"); 1027} 1028 1029- (IBAction)aboutDialogClicked:(id)sender { 1030 MacProcess::LaunchMozcTool("about_dialog"); 1031} 1032 1033- (void)outputResult:(mozc::commands::Output *)output { 1034 if (output == nullptr || !output->has_result()) { 1035 return; 1036 } 1037 [self commitText:output->result().value().c_str() client:[self client]]; 1038} 1039@end 1040