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