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