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 24 using base::TimeTicks; 25 using content::BrowserMessageFilter; 26 using content::BrowserThread; 27 28 namespace { 29 // The number of characters in the first part of the language code. 30 const 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. 38 NSSpellChecker* 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. 47 NSString* 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 85 std::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 103 namespace spellcheck_mac { 104 105 void 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 113 bool 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 119 bool SpellCheckerProvidesPanel() { 120 // OS X has a Spelling Panel, so we can return true here. 121 return true; 122 } 123 124 bool SpellingPanelVisible() { 125 // This should only be called from the main thread. 126 DCHECK([NSThread currentThread] == [NSThread mainThread]); 127 return [[SharedSpellChecker() spellingPanel] isVisible]; 128 } 129 130 void 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 144 void 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 153 bool 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 164 void 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 173 static int last_seen_tag_; 174 175 bool 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 196 void 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 209 void AddWord(const base::string16& word) { 210 NSString* word_to_add = base::SysUTF16ToNSString(word); 211 [SharedSpellChecker() learnWord:word_to_add]; 212 } 213 214 void RemoveWord(const base::string16& word) { 215 NSString *word_to_remove = base::SysUTF16ToNSString(word); 216 [SharedSpellChecker() unlearnWord:word_to_remove]; 217 } 218 219 int GetDocumentTag() { 220 NSInteger doc_tag = [NSSpellChecker uniqueSpellDocumentTag]; 221 return static_cast<int>(doc_tag); 222 } 223 224 void IgnoreWord(const base::string16& word) { 225 [SharedSpellChecker() ignoreWord:base::SysUTF16ToNSString(word) 226 inSpellDocumentWithTag:last_seen_tag_]; 227 } 228 229 void CloseDocumentWithTag(int tag) { 230 [SharedSpellChecker() closeSpellDocumentWithTag:static_cast<NSInteger>(tag)]; 231 } 232 233 void 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 269 class SpellcheckerStateInternal { 270 public: 271 SpellcheckerStateInternal(); 272 ~SpellcheckerStateInternal(); 273 274 private: 275 BOOL automaticallyIdentifiesLanguages_; 276 NSString* language_; 277 }; 278 279 SpellcheckerStateInternal::SpellcheckerStateInternal() { 280 language_ = [SharedSpellChecker() language]; 281 automaticallyIdentifiesLanguages_ = 282 [SharedSpellChecker() automaticallyIdentifiesLanguages]; 283 [SharedSpellChecker() setLanguage:@"en"]; 284 [SharedSpellChecker() setAutomaticallyIdentifiesLanguages:NO]; 285 } 286 287 SpellcheckerStateInternal::~SpellcheckerStateInternal() { 288 [SharedSpellChecker() setLanguage:language_]; 289 [SharedSpellChecker() setAutomaticallyIdentifiesLanguages: 290 automaticallyIdentifiesLanguages_]; 291 } 292 293 ScopedEnglishLanguageForTest::ScopedEnglishLanguageForTest() 294 : state_(new SpellcheckerStateInternal) { 295 } 296 297 ScopedEnglishLanguageForTest::~ScopedEnglishLanguageForTest() { 298 delete state_; 299 } 300 301 } // namespace spellcheck_mac 302