| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // Integration with OS X native spellchecker. |
| |
| #include "chrome/browser/spellchecker/spellcheck_platform_mac.h" |
| |
| #import <Cocoa/Cocoa.h> |
| |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/logging.h" |
| #include "base/mac/foundation_util.h" |
| #include "base/mac/scoped_nsexception_enabler.h" |
| #include "base/metrics/histogram.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/time/time.h" |
| #include "chrome/common/spellcheck_common.h" |
| #include "chrome/common/spellcheck_messages.h" |
| #include "chrome/common/spellcheck_result.h" |
| #include "content/public/browser/browser_message_filter.h" |
| #include "content/public/browser/browser_thread.h" |
| |
| using base::TimeTicks; |
| using content::BrowserMessageFilter; |
| using content::BrowserThread; |
| |
| namespace { |
| // The number of characters in the first part of the language code. |
| const unsigned int kShortLanguageCodeSize = 2; |
| |
| // +[NSSpellChecker sharedSpellChecker] can throw exceptions depending |
| // on the state of the pasteboard, or possibly as a result of |
| // third-party code (when setting up services entries). The following |
| // receives nil if an exception is thrown, in which case |
| // spell-checking will not work, but it also will not crash the |
| // browser. |
| NSSpellChecker* SharedSpellChecker() { |
| return base::mac::ObjCCastStrict<NSSpellChecker>( |
| base::mac::RunBlockIgnoringExceptions(^{ |
| return [NSSpellChecker sharedSpellChecker]; |
| })); |
| } |
| |
| // A private utility function to convert hunspell language codes to OS X |
| // language codes. |
| NSString* ConvertLanguageCodeToMac(const std::string& hunspell_lang_code) { |
| NSString* whole_code = base::SysUTF8ToNSString(hunspell_lang_code); |
| |
| if ([whole_code length] > kShortLanguageCodeSize) { |
| NSString* lang_code = [whole_code |
| substringToIndex:kShortLanguageCodeSize]; |
| // Add 1 here to skip the underscore. |
| NSString* region_code = [whole_code |
| substringFromIndex:(kShortLanguageCodeSize + 1)]; |
| |
| // Check for the special case of en-US and pt-PT, since OS X lists these |
| // as just en and pt respectively. |
| // TODO(pwicks): Find out if there are other special cases for languages |
| // not installed on the system by default. Are there others like pt-PT? |
| if (([lang_code isEqualToString:@"en"] && |
| [region_code isEqualToString:@"US"]) || |
| ([lang_code isEqualToString:@"pt"] && |
| [region_code isEqualToString:@"PT"])) { |
| return lang_code; |
| } |
| |
| // Otherwise, just build a string that uses an underscore instead of a |
| // dash between the language and the region code, since this is the |
| // format that OS X uses. |
| NSString* os_x_language = |
| [NSString stringWithFormat:@"%@_%@", lang_code, region_code]; |
| return os_x_language; |
| } else { |
| // Special case for Polish. |
| if ([whole_code isEqualToString:@"pl"]) { |
| return @"pl_PL"; |
| } |
| // This is just a language code with the same format as OS X |
| // language code. |
| return whole_code; |
| } |
| } |
| |
| std::string ConvertLanguageCodeFromMac(NSString* lang_code) { |
| // TODO(pwicks):figure out what to do about Multilingual |
| // Guards for strange cases. |
| if ([lang_code isEqualToString:@"en"]) return std::string("en-US"); |
| if ([lang_code isEqualToString:@"pt"]) return std::string("pt-PT"); |
| if ([lang_code isEqualToString:@"pl_PL"]) return std::string("pl"); |
| |
| if ([lang_code length] > kShortLanguageCodeSize && |
| [lang_code characterAtIndex:kShortLanguageCodeSize] == '_') { |
| return base::SysNSStringToUTF8([NSString stringWithFormat:@"%@-%@", |
| [lang_code substringToIndex:kShortLanguageCodeSize], |
| [lang_code substringFromIndex:(kShortLanguageCodeSize + 1)]]); |
| } |
| return base::SysNSStringToUTF8(lang_code); |
| } |
| |
| } // namespace |
| |
| namespace spellcheck_mac { |
| |
| void GetAvailableLanguages(std::vector<std::string>* spellcheck_languages) { |
| NSArray* availableLanguages = [SharedSpellChecker() availableLanguages]; |
| for (NSString* lang_code in availableLanguages) { |
| spellcheck_languages->push_back( |
| ConvertLanguageCodeFromMac(lang_code)); |
| } |
| } |
| |
| bool SpellCheckerAvailable() { |
| // If this file was compiled, then we know that we are on OS X 10.5 at least |
| // and can safely return true here. |
| return true; |
| } |
| |
| bool SpellCheckerProvidesPanel() { |
| // OS X has a Spelling Panel, so we can return true here. |
| return true; |
| } |
| |
| bool SpellingPanelVisible() { |
| // This should only be called from the main thread. |
| DCHECK([NSThread currentThread] == [NSThread mainThread]); |
| return [[SharedSpellChecker() spellingPanel] isVisible]; |
| } |
| |
| void ShowSpellingPanel(bool show) { |
| if (show) { |
| [[SharedSpellChecker() spellingPanel] |
| performSelectorOnMainThread:@selector(makeKeyAndOrderFront:) |
| withObject:nil |
| waitUntilDone:YES]; |
| } else { |
| [[SharedSpellChecker() spellingPanel] |
| performSelectorOnMainThread:@selector(close) |
| withObject:nil |
| waitUntilDone:YES]; |
| } |
| } |
| |
| void UpdateSpellingPanelWithMisspelledWord(const base::string16& word) { |
| NSString * word_to_display = base::SysUTF16ToNSString(word); |
| [SharedSpellChecker() |
| performSelectorOnMainThread: |
| @selector(updateSpellingPanelWithMisspelledWord:) |
| withObject:word_to_display |
| waitUntilDone:YES]; |
| } |
| |
| bool PlatformSupportsLanguage(const std::string& current_language) { |
| // First, convert the language to an OS X language code. |
| NSString* mac_lang_code = ConvertLanguageCodeToMac(current_language); |
| |
| // Then grab the languages available. |
| NSArray* availableLanguages = [SharedSpellChecker() availableLanguages]; |
| |
| // Return true if the given language is supported by OS X. |
| return [availableLanguages containsObject:mac_lang_code]; |
| } |
| |
| void SetLanguage(const std::string& lang_to_set) { |
| // Do not set any language right now, since Chrome should honor the |
| // system spellcheck settings. (http://crbug.com/166046) |
| // Fix this once Chrome actually allows setting a spellcheck language |
| // in chrome://settings. |
| // NSString* NS_lang_to_set = ConvertLanguageCodeToMac(lang_to_set); |
| // [SharedSpellChecker() setLanguage:NS_lang_to_set]; |
| } |
| |
| static int last_seen_tag_; |
| |
| bool CheckSpelling(const base::string16& word_to_check, int tag) { |
| last_seen_tag_ = tag; |
| |
| // -[NSSpellChecker checkSpellingOfString] returns an NSRange that |
| // we can look at to determine if a word is misspelled. |
| NSRange spell_range = {0,0}; |
| |
| // Convert the word to an NSString. |
| NSString* NS_word_to_check = base::SysUTF16ToNSString(word_to_check); |
| // Check the spelling, starting at the beginning of the word. |
| spell_range = [SharedSpellChecker() |
| checkSpellingOfString:NS_word_to_check startingAt:0 |
| language:nil wrap:NO inSpellDocumentWithTag:tag |
| wordCount:NULL]; |
| |
| // If the length of the misspelled word == 0, |
| // then there is no misspelled word. |
| bool word_correct = (spell_range.length == 0); |
| return word_correct; |
| } |
| |
| void FillSuggestionList(const base::string16& wrong_word, |
| std::vector<base::string16>* optional_suggestions) { |
| NSString* NS_wrong_word = base::SysUTF16ToNSString(wrong_word); |
| TimeTicks debug_begin_time = base::Histogram::DebugNow(); |
| // The suggested words for |wrong_word|. |
| NSArray* guesses = [SharedSpellChecker() guessesForWord:NS_wrong_word]; |
| DHISTOGRAM_TIMES("Spellcheck.SuggestTime", |
| base::Histogram::DebugNow() - debug_begin_time); |
| |
| for (int i = 0; i < static_cast<int>([guesses count]); ++i) { |
| if (i < chrome::spellcheck_common::kMaxSuggestions) { |
| optional_suggestions->push_back(base::SysNSStringToUTF16( |
| [guesses objectAtIndex:i])); |
| } |
| } |
| } |
| |
| void AddWord(const base::string16& word) { |
| NSString* word_to_add = base::SysUTF16ToNSString(word); |
| [SharedSpellChecker() learnWord:word_to_add]; |
| } |
| |
| void RemoveWord(const base::string16& word) { |
| NSString *word_to_remove = base::SysUTF16ToNSString(word); |
| [SharedSpellChecker() unlearnWord:word_to_remove]; |
| } |
| |
| int GetDocumentTag() { |
| NSInteger doc_tag = [NSSpellChecker uniqueSpellDocumentTag]; |
| return static_cast<int>(doc_tag); |
| } |
| |
| void IgnoreWord(const base::string16& word) { |
| [SharedSpellChecker() ignoreWord:base::SysUTF16ToNSString(word) |
| inSpellDocumentWithTag:last_seen_tag_]; |
| } |
| |
| void CloseDocumentWithTag(int tag) { |
| [SharedSpellChecker() closeSpellDocumentWithTag:static_cast<NSInteger>(tag)]; |
| } |
| |
| void RequestTextCheck(int document_tag, |
| const base::string16& text, |
| TextCheckCompleteCallback callback) { |
| NSString* text_to_check = base::SysUTF16ToNSString(text); |
| NSRange range_to_check = NSMakeRange(0, [text_to_check length]); |
| |
| [SharedSpellChecker() |
| requestCheckingOfString:text_to_check |
| range:range_to_check |
| types:NSTextCheckingTypeSpelling |
| options:nil |
| inSpellDocumentWithTag:document_tag |
| completionHandler:^(NSInteger, |
| NSArray *results, |
| NSOrthography*, |
| NSInteger) { |
| std::vector<SpellCheckResult> check_results; |
| for (NSTextCheckingResult* result in results) { |
| // Deliberately ignore non-spelling results. OSX at the very least |
| // delivers a result of NSTextCheckingTypeOrthography for the |
| // whole fragment, which underlines the entire checked range. |
| if ([result resultType] != NSTextCheckingTypeSpelling) |
| continue; |
| |
| // In this use case, the spell checker should never |
| // return anything but a single range per result. |
| check_results.push_back(SpellCheckResult( |
| SpellCheckResult::SPELLING, |
| [result range].location, |
| [result range].length)); |
| } |
| // TODO(groby): Verify we don't need to post from here. |
| callback.Run(check_results); |
| }]; |
| } |
| |
| class SpellcheckerStateInternal { |
| public: |
| SpellcheckerStateInternal(); |
| ~SpellcheckerStateInternal(); |
| |
| private: |
| BOOL automaticallyIdentifiesLanguages_; |
| NSString* language_; |
| }; |
| |
| SpellcheckerStateInternal::SpellcheckerStateInternal() { |
| language_ = [SharedSpellChecker() language]; |
| automaticallyIdentifiesLanguages_ = |
| [SharedSpellChecker() automaticallyIdentifiesLanguages]; |
| [SharedSpellChecker() setLanguage:@"en"]; |
| [SharedSpellChecker() setAutomaticallyIdentifiesLanguages:NO]; |
| } |
| |
| SpellcheckerStateInternal::~SpellcheckerStateInternal() { |
| [SharedSpellChecker() setLanguage:language_]; |
| [SharedSpellChecker() setAutomaticallyIdentifiesLanguages: |
| automaticallyIdentifiesLanguages_]; |
| } |
| |
| ScopedEnglishLanguageForTest::ScopedEnglishLanguageForTest() |
| : state_(new SpellcheckerStateInternal) { |
| } |
| |
| ScopedEnglishLanguageForTest::~ScopedEnglishLanguageForTest() { |
| delete state_; |
| } |
| |
| } // namespace spellcheck_mac |