1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5// Integration with OS X native spellchecker.
6
7#include "chrome/browser/spellchecker/spellcheck_platform_mac.h"
8
9#import <Cocoa/Cocoa.h>
10
11#include "base/bind.h"
12#include "base/bind_helpers.h"
13#include "base/logging.h"
14#include "base/mac/foundation_util.h"
15#include "base/mac/scoped_nsexception_enabler.h"
16#include "base/strings/sys_string_conversions.h"
17#include "base/time/time.h"
18#include "chrome/common/spellcheck_common.h"
19#include "chrome/common/spellcheck_messages.h"
20#include "chrome/common/spellcheck_result.h"
21#include "content/public/browser/browser_message_filter.h"
22#include "content/public/browser/browser_thread.h"
23
24using base::TimeTicks;
25using content::BrowserMessageFilter;
26using content::BrowserThread;
27
28namespace {
29// The number of characters in the first part of the language code.
30const unsigned int kShortLanguageCodeSize = 2;
31
32// +[NSSpellChecker sharedSpellChecker] can throw exceptions depending
33// on the state of the pasteboard, or possibly as a result of
34// third-party code (when setting up services entries).  The following
35// receives nil if an exception is thrown, in which case
36// spell-checking will not work, but it also will not crash the
37// browser.
38NSSpellChecker* SharedSpellChecker() {
39  return base::mac::ObjCCastStrict<NSSpellChecker>(
40      base::mac::RunBlockIgnoringExceptions(^{
41          return [NSSpellChecker sharedSpellChecker];
42      }));
43}
44
45// A private utility function to convert hunspell language codes to OS X
46// language codes.
47NSString* ConvertLanguageCodeToMac(const std::string& hunspell_lang_code) {
48  NSString* whole_code = base::SysUTF8ToNSString(hunspell_lang_code);
49
50  if ([whole_code length] > kShortLanguageCodeSize) {
51    NSString* lang_code = [whole_code
52                           substringToIndex:kShortLanguageCodeSize];
53    // Add 1 here to skip the underscore.
54    NSString* region_code = [whole_code
55                             substringFromIndex:(kShortLanguageCodeSize + 1)];
56
57    // Check for the special case of en-US and pt-PT, since OS X lists these
58    // as just en and pt respectively.
59    // TODO(pwicks): Find out if there are other special cases for languages
60    // not installed on the system by default. Are there others like pt-PT?
61    if (([lang_code isEqualToString:@"en"] &&
62       [region_code isEqualToString:@"US"]) ||
63        ([lang_code isEqualToString:@"pt"] &&
64       [region_code isEqualToString:@"PT"])) {
65      return lang_code;
66    }
67
68    // Otherwise, just build a string that uses an underscore instead of a
69    // dash between the language and the region code, since this is the
70    // format that OS X uses.
71    NSString* os_x_language =
72        [NSString stringWithFormat:@"%@_%@", lang_code, region_code];
73    return os_x_language;
74  } else {
75    // Special case for Polish.
76    if ([whole_code isEqualToString:@"pl"]) {
77      return @"pl_PL";
78    }
79    // This is just a language code with the same format as OS X
80    // language code.
81    return whole_code;
82  }
83}
84
85std::string ConvertLanguageCodeFromMac(NSString* lang_code) {
86  // TODO(pwicks):figure out what to do about Multilingual
87  // Guards for strange cases.
88  if ([lang_code isEqualToString:@"en"]) return std::string("en-US");
89  if ([lang_code isEqualToString:@"pt"]) return std::string("pt-PT");
90  if ([lang_code isEqualToString:@"pl_PL"]) return std::string("pl");
91
92  if ([lang_code length] > kShortLanguageCodeSize &&
93      [lang_code characterAtIndex:kShortLanguageCodeSize] == '_') {
94    return base::SysNSStringToUTF8([NSString stringWithFormat:@"%@-%@",
95                [lang_code substringToIndex:kShortLanguageCodeSize],
96                [lang_code substringFromIndex:(kShortLanguageCodeSize + 1)]]);
97  }
98  return base::SysNSStringToUTF8(lang_code);
99}
100
101} // namespace
102
103namespace spellcheck_mac {
104
105void GetAvailableLanguages(std::vector<std::string>* spellcheck_languages) {
106  NSArray* availableLanguages = [SharedSpellChecker() availableLanguages];
107  for (NSString* lang_code in availableLanguages) {
108    spellcheck_languages->push_back(
109              ConvertLanguageCodeFromMac(lang_code));
110  }
111}
112
113bool SpellCheckerAvailable() {
114  // If this file was compiled, then we know that we are on OS X 10.5 at least
115  // and can safely return true here.
116  return true;
117}
118
119bool SpellCheckerProvidesPanel() {
120  // OS X has a Spelling Panel, so we can return true here.
121  return true;
122}
123
124bool SpellingPanelVisible() {
125  // This should only be called from the main thread.
126  DCHECK([NSThread currentThread] == [NSThread mainThread]);
127  return [[SharedSpellChecker() spellingPanel] isVisible];
128}
129
130void ShowSpellingPanel(bool show) {
131  if (show) {
132    [[SharedSpellChecker() spellingPanel]
133        performSelectorOnMainThread:@selector(makeKeyAndOrderFront:)
134                         withObject:nil
135                      waitUntilDone:YES];
136  } else {
137    [[SharedSpellChecker() spellingPanel]
138        performSelectorOnMainThread:@selector(close)
139                         withObject:nil
140                      waitUntilDone:YES];
141  }
142}
143
144void UpdateSpellingPanelWithMisspelledWord(const base::string16& word) {
145  NSString * word_to_display = base::SysUTF16ToNSString(word);
146  [SharedSpellChecker()
147      performSelectorOnMainThread:
148        @selector(updateSpellingPanelWithMisspelledWord:)
149                       withObject:word_to_display
150                    waitUntilDone:YES];
151}
152
153bool PlatformSupportsLanguage(const std::string& current_language) {
154  // First, convert the language to an OS X language code.
155  NSString* mac_lang_code = ConvertLanguageCodeToMac(current_language);
156
157  // Then grab the languages available.
158  NSArray* availableLanguages = [SharedSpellChecker() availableLanguages];
159
160  // Return true if the given language is supported by OS X.
161  return [availableLanguages containsObject:mac_lang_code];
162}
163
164void SetLanguage(const std::string& lang_to_set) {
165  // Do not set any language right now, since Chrome should honor the
166  // system spellcheck settings. (http://crbug.com/166046)
167  // Fix this once Chrome actually allows setting a spellcheck language
168  // in chrome://settings.
169  //  NSString* NS_lang_to_set = ConvertLanguageCodeToMac(lang_to_set);
170  //  [SharedSpellChecker() setLanguage:NS_lang_to_set];
171}
172
173static int last_seen_tag_;
174
175bool CheckSpelling(const base::string16& word_to_check, int tag) {
176  last_seen_tag_ = tag;
177
178  // -[NSSpellChecker checkSpellingOfString] returns an NSRange that
179  // we can look at to determine if a word is misspelled.
180  NSRange spell_range = {0,0};
181
182  // Convert the word to an NSString.
183  NSString* NS_word_to_check = base::SysUTF16ToNSString(word_to_check);
184  // Check the spelling, starting at the beginning of the word.
185  spell_range = [SharedSpellChecker()
186                  checkSpellingOfString:NS_word_to_check startingAt:0
187                  language:nil wrap:NO inSpellDocumentWithTag:tag
188                  wordCount:NULL];
189
190  // If the length of the misspelled word == 0,
191  // then there is no misspelled word.
192  bool word_correct = (spell_range.length == 0);
193  return word_correct;
194}
195
196void FillSuggestionList(const base::string16& wrong_word,
197                        std::vector<base::string16>* optional_suggestions) {
198  NSString* NS_wrong_word = base::SysUTF16ToNSString(wrong_word);
199  // The suggested words for |wrong_word|.
200  NSArray* guesses = [SharedSpellChecker() guessesForWord:NS_wrong_word];
201  for (int i = 0; i < static_cast<int>([guesses count]); ++i) {
202    if (i < chrome::spellcheck_common::kMaxSuggestions) {
203      optional_suggestions->push_back(base::SysNSStringToUTF16(
204                                      [guesses objectAtIndex:i]));
205    }
206  }
207}
208
209void AddWord(const base::string16& word) {
210    NSString* word_to_add = base::SysUTF16ToNSString(word);
211  [SharedSpellChecker() learnWord:word_to_add];
212}
213
214void RemoveWord(const base::string16& word) {
215  NSString *word_to_remove = base::SysUTF16ToNSString(word);
216  [SharedSpellChecker() unlearnWord:word_to_remove];
217}
218
219int GetDocumentTag() {
220  NSInteger doc_tag = [NSSpellChecker uniqueSpellDocumentTag];
221  return static_cast<int>(doc_tag);
222}
223
224void IgnoreWord(const base::string16& word) {
225  [SharedSpellChecker() ignoreWord:base::SysUTF16ToNSString(word)
226            inSpellDocumentWithTag:last_seen_tag_];
227}
228
229void CloseDocumentWithTag(int tag) {
230  [SharedSpellChecker() closeSpellDocumentWithTag:static_cast<NSInteger>(tag)];
231}
232
233void RequestTextCheck(int document_tag,
234                      const base::string16& text,
235                      TextCheckCompleteCallback callback) {
236  NSString* text_to_check = base::SysUTF16ToNSString(text);
237  NSRange range_to_check = NSMakeRange(0, [text_to_check length]);
238
239  [SharedSpellChecker()
240      requestCheckingOfString:text_to_check
241                        range:range_to_check
242                        types:NSTextCheckingTypeSpelling
243                      options:nil
244       inSpellDocumentWithTag:document_tag
245            completionHandler:^(NSInteger,
246                                NSArray *results,
247                                NSOrthography*,
248                                NSInteger) {
249          std::vector<SpellCheckResult> check_results;
250          for (NSTextCheckingResult* result in results) {
251            // Deliberately ignore non-spelling results. OSX at the very least
252            // delivers a result of NSTextCheckingTypeOrthography for the
253            // whole fragment, which underlines the entire checked range.
254            if ([result resultType] != NSTextCheckingTypeSpelling)
255              continue;
256
257            // In this use case, the spell checker should never
258            // return anything but a single range per result.
259            check_results.push_back(SpellCheckResult(
260                SpellCheckResult::SPELLING,
261                [result range].location,
262                [result range].length));
263          }
264          // TODO(groby): Verify we don't need to post from here.
265          callback.Run(check_results);
266      }];
267}
268
269class SpellcheckerStateInternal {
270 public:
271  SpellcheckerStateInternal();
272  ~SpellcheckerStateInternal();
273
274 private:
275  BOOL automaticallyIdentifiesLanguages_;
276  NSString* language_;
277};
278
279SpellcheckerStateInternal::SpellcheckerStateInternal() {
280  language_ = [SharedSpellChecker() language];
281  automaticallyIdentifiesLanguages_ =
282      [SharedSpellChecker() automaticallyIdentifiesLanguages];
283  [SharedSpellChecker() setLanguage:@"en"];
284  [SharedSpellChecker() setAutomaticallyIdentifiesLanguages:NO];
285}
286
287SpellcheckerStateInternal::~SpellcheckerStateInternal() {
288  [SharedSpellChecker() setLanguage:language_];
289  [SharedSpellChecker() setAutomaticallyIdentifiesLanguages:
290      automaticallyIdentifiesLanguages_];
291}
292
293ScopedEnglishLanguageForTest::ScopedEnglishLanguageForTest()
294    : state_(new SpellcheckerStateInternal) {
295}
296
297ScopedEnglishLanguageForTest::~ScopedEnglishLanguageForTest() {
298  delete state_;
299}
300
301}  // namespace spellcheck_mac
302