Home | History | Annotate | Download | only in spellchecker
      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 
     25 using base::TimeTicks;
     26 using content::BrowserMessageFilter;
     27 using content::BrowserThread;
     28 
     29 namespace {
     30 // The number of characters in the first part of the language code.
     31 const 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.
     39 NSSpellChecker* 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.
     48 NSString* 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 
     86 std::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 
    104 namespace spellcheck_mac {
    105 
    106 void 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 
    114 bool 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 
    120 bool SpellCheckerProvidesPanel() {
    121   // OS X has a Spelling Panel, so we can return true here.
    122   return true;
    123 }
    124 
    125 bool SpellingPanelVisible() {
    126   // This should only be called from the main thread.
    127   DCHECK([NSThread currentThread] == [NSThread mainThread]);
    128   return [[SharedSpellChecker() spellingPanel] isVisible];
    129 }
    130 
    131 void 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 
    145 void 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 
    154 bool 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 
    165 void 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 
    174 static int last_seen_tag_;
    175 
    176 bool 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 
    197 void 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 
    214 void AddWord(const base::string16& word) {
    215     NSString* word_to_add = base::SysUTF16ToNSString(word);
    216   [SharedSpellChecker() learnWord:word_to_add];
    217 }
    218 
    219 void RemoveWord(const base::string16& word) {
    220   NSString *word_to_remove = base::SysUTF16ToNSString(word);
    221   [SharedSpellChecker() unlearnWord:word_to_remove];
    222 }
    223 
    224 int GetDocumentTag() {
    225   NSInteger doc_tag = [NSSpellChecker uniqueSpellDocumentTag];
    226   return static_cast<int>(doc_tag);
    227 }
    228 
    229 void IgnoreWord(const base::string16& word) {
    230   [SharedSpellChecker() ignoreWord:base::SysUTF16ToNSString(word)
    231             inSpellDocumentWithTag:last_seen_tag_];
    232 }
    233 
    234 void CloseDocumentWithTag(int tag) {
    235   [SharedSpellChecker() closeSpellDocumentWithTag:static_cast<NSInteger>(tag)];
    236 }
    237 
    238 void 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 
    274 class SpellcheckerStateInternal {
    275  public:
    276   SpellcheckerStateInternal();
    277   ~SpellcheckerStateInternal();
    278 
    279  private:
    280   BOOL automaticallyIdentifiesLanguages_;
    281   NSString* language_;
    282 };
    283 
    284 SpellcheckerStateInternal::SpellcheckerStateInternal() {
    285   language_ = [SharedSpellChecker() language];
    286   automaticallyIdentifiesLanguages_ =
    287       [SharedSpellChecker() automaticallyIdentifiesLanguages];
    288   [SharedSpellChecker() setLanguage:@"en"];
    289   [SharedSpellChecker() setAutomaticallyIdentifiesLanguages:NO];
    290 }
    291 
    292 SpellcheckerStateInternal::~SpellcheckerStateInternal() {
    293   [SharedSpellChecker() setLanguage:language_];
    294   [SharedSpellChecker() setAutomaticallyIdentifiesLanguages:
    295       automaticallyIdentifiesLanguages_];
    296 }
    297 
    298 ScopedEnglishLanguageForTest::ScopedEnglishLanguageForTest()
    299     : state_(new SpellcheckerStateInternal) {
    300 }
    301 
    302 ScopedEnglishLanguageForTest::~ScopedEnglishLanguageForTest() {
    303   delete state_;
    304 }
    305 
    306 }  // namespace spellcheck_mac
    307