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