text: // Copyright 2014 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.
  
  #include "chrome/browser/renderer_context_menu/spelling_menu_observer.h"
  
  #include "base/bind.h"
  #include "base/command_line.h"
  #include "base/i18n/case_conversion.h"
  #include "base/prefs/pref_service.h"
  #include "base/strings/utf_string_conversions.h"
  #include "chrome/app/chrome_command_ids.h"
  #include "chrome/browser/profiles/profile.h"
  #include "chrome/browser/renderer_context_menu/render_view_context_menu.h"
  #include "chrome/browser/renderer_context_menu/spelling_bubble_model.h"
  #include "chrome/browser/spellchecker/spellcheck_factory.h"
  #include "chrome/browser/spellchecker/spellcheck_host_metrics.h"
  #include "chrome/browser/spellchecker/spellcheck_platform_mac.h"
  #include "chrome/browser/spellchecker/spellcheck_service.h"
  #include "chrome/browser/spellchecker/spelling_service_client.h"
  #include "chrome/browser/ui/confirm_bubble.h"
  #include "chrome/common/chrome_switches.h"
  #include "chrome/common/pref_names.h"
  #include "chrome/common/spellcheck_result.h"
  #include "content/public/browser/render_view_host.h"
  #include "content/public/browser/render_widget_host_view.h"
  #include "content/public/browser/web_contents.h"
  #include "content/public/browser/web_contents_view.h"
  #include "content/public/common/context_menu_params.h"
  #include "extensions/browser/view_type_utils.h"
  #include "grit/generated_resources.h"
  #include "ui/base/l10n/l10n_util.h"
  #include "ui/gfx/rect.h"
  
  using content::BrowserThread;
  
  SpellingMenuObserver::SpellingMenuObserver(RenderViewContextMenuProxy* proxy)
      : proxy_(proxy),
        loading_frame_(0),
        succeeded_(false),
        misspelling_hash_(0),
        client_(new SpellingServiceClient) {
    if (proxy && proxy->GetProfile()) {
      integrate_spelling_service_.Init(prefs::kSpellCheckUseSpellingService,
                                       proxy->GetProfile()->GetPrefs());
      autocorrect_spelling_.Init(prefs::kEnableAutoSpellCorrect,
                                 proxy->GetProfile()->GetPrefs());
    }
  }
  
  SpellingMenuObserver::~SpellingMenuObserver() {
  }
  
  void SpellingMenuObserver::InitMenu(const content::ContextMenuParams& params) {
    DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    DCHECK(!params.misspelled_word.empty() ||
        params.dictionary_suggestions.empty());
  
    // Exit if we are not in an editable element because we add a menu item only
    // for editable elements.
    Profile* profile = proxy_->GetProfile();
    if (!params.is_editable || !profile)
      return;
  
    // Exit if there is no misspelled word.
    if (params.misspelled_word.empty())
      return;
  
    suggestions_ = params.dictionary_suggestions;
    misspelled_word_ = params.misspelled_word;
    misspelling_hash_ = params.misspelling_hash;
  
    bool use_suggestions = SpellingServiceClient::IsAvailable(
        profile, SpellingServiceClient::SUGGEST);
  
    if (!suggestions_.empty() || use_suggestions)
      proxy_->AddSeparator();
  
    // Append Dictionary spell check suggestions.
    for (size_t i = 0; i < params.dictionary_suggestions.size() &&
         IDC_SPELLCHECK_SUGGESTION_0 + i <= IDC_SPELLCHECK_SUGGESTION_LAST;
         ++i) {
      proxy_->AddMenuItem(IDC_SPELLCHECK_SUGGESTION_0 + static_cast<int>(i),
                          params.dictionary_suggestions[i]);
    }
  
    // The service types |SpellingServiceClient::SPELLCHECK| and
    // |SpellingServiceClient::SUGGEST| are mutually exclusive. Only one is
    // available at at time.
    //
    // When |SpellingServiceClient::SPELLCHECK| is available, the contextual
    // suggestions from |SpellingServiceClient| are already stored in
    // |params.dictionary_suggestions|.  |SpellingMenuObserver| places these
    // suggestions in the slots |IDC_SPELLCHECK_SUGGESTION_[0-LAST]|. If
    // |SpellingMenuObserver| queried |SpellingServiceClient| again, then quality
    // of suggestions would be reduced by lack of context around the misspelled
    // word.
    //
    // When |SpellingServiceClient::SUGGEST| is available,
    // |params.dictionary_suggestions| contains suggestions only from Hunspell
    // dictionary. |SpellingMenuObserver| queries |SpellingServiceClient| with the
    // misspelled word without the surrounding context. Spellcheck suggestions
    // from |SpellingServiceClient::SUGGEST| are not available until
    // |SpellingServiceClient| responds to the query. While |SpellingMenuObserver|
    // waits for |SpellingServiceClient|, it shows a placeholder text "Loading
    // suggestion..." in the |IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION| slot. After
    // |SpellingServiceClient| responds to the query, |SpellingMenuObserver|
    // replaces the placeholder text with either the spelling suggestion or the
    // message "No more suggestions from Google." The "No more suggestions"
    // message is there when |SpellingServiceClient| returned the same suggestion
    // as Hunspell.
    if (use_suggestions) {
      // Append a placeholder item for the suggestion from the Spelling service
      // and send a request to the service if we can retrieve suggestions from it.
      // Also, see if we can use the spelling service to get an ideal suggestion.
      // Otherwise, we'll fall back to the set of suggestions.  Initialize
      // variables used in OnTextCheckComplete(). We copy the input text to the
      // result text so we can replace its misspelled regions with suggestions.
      succeeded_ = false;
      result_ = params.misspelled_word;
  
      // Add a placeholder item. This item will be updated when we receive a
      // response from the Spelling service. (We do not have to disable this
      // item now since Chrome will call IsCommandIdEnabled() and disable it.)
      loading_message_ =
          l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_SPELLING_CHECKING);
      proxy_->AddMenuItem(IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION,
                          loading_message_);
      // Invoke a JSON-RPC call to the Spelling service in the background so we
      // can update the placeholder item when we receive its response. It also
      // starts the animation timer so we can show animation until we receive
      // it.
      bool result = client_->RequestTextCheck(
          profile, SpellingServiceClient::SUGGEST, params.misspelled_word,
          base::Bind(&SpellingMenuObserver::OnTextCheckComplete,
                     base::Unretained(this), SpellingServiceClient::SUGGEST));
      if (result) {
        loading_frame_ = 0;
        animation_timer_.Start(FROM_HERE, base::TimeDelta::FromSeconds(1),
            this, &SpellingMenuObserver::OnAnimationTimerExpired);
      }
    }
  
    if (params.dictionary_suggestions.empty()) {
      proxy_->AddMenuItem(
          IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS,
          l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS));
      bool use_spelling_service = SpellingServiceClient::IsAvailable(
          profile, SpellingServiceClient::SPELLCHECK);
      if (use_suggestions || use_spelling_service)
        proxy_->AddSeparator();
    } else {
      proxy_->AddSeparator();
  
      // |spellcheck_service| can be null when the suggested word is
      // provided by Web SpellCheck API.
      SpellcheckService* spellcheck_service =
          SpellcheckServiceFactory::GetForContext(profile);
      if (spellcheck_service && spellcheck_service->GetMetrics())
        spellcheck_service->GetMetrics()->RecordSuggestionStats(1);
    }
  
    // If word is misspelled, give option for "Add to dictionary" and a check item
    // "Ask Google for suggestions".
    proxy_->AddMenuItem(IDC_SPELLCHECK_ADD_TO_DICTIONARY,
        l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_ADD_TO_DICTIONARY));
  
  #if defined(TOOLKIT_GTK)
    extensions::ViewType view_type =
        extensions::GetViewType(proxy_->GetWebContents());
    if (view_type != extensions::VIEW_TYPE_PANEL) {
  #endif
      proxy_->AddCheckItem(IDC_CONTENT_CONTEXT_SPELLING_TOGGLE,
          l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_SPELLING_ASK_GOOGLE));
  #if defined(TOOLKIT_GTK)
    }
  #endif
  
    const CommandLine* command_line = CommandLine::ForCurrentProcess();
    if (command_line->HasSwitch(switches::kEnableSpellingAutoCorrect)) {
      proxy_->AddCheckItem(IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE,
          l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_SPELLING_AUTOCORRECT));
    }
  
    proxy_->AddSeparator();
  }
  
  bool SpellingMenuObserver::IsCommandIdSupported(int command_id) {
    if (command_id >= IDC_SPELLCHECK_SUGGESTION_0 &&
        command_id <= IDC_SPELLCHECK_SUGGESTION_LAST)
      return true;
  
    switch (command_id) {
      case IDC_SPELLCHECK_ADD_TO_DICTIONARY:
      case IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS:
      case IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION:
      case IDC_CONTENT_CONTEXT_SPELLING_TOGGLE:
      case IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE:
        return true;
  
      default:
        return false;
    }
    return false;
  }
  
  bool SpellingMenuObserver::IsCommandIdChecked(int command_id) {
    DCHECK(IsCommandIdSupported(command_id));
  
    if (command_id == IDC_CONTENT_CONTEXT_SPELLING_TOGGLE)
      return integrate_spelling_service_.GetValue() &&
          !proxy_->GetProfile()->IsOffTheRecord();
    else if (command_id == IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE)
      return autocorrect_spelling_.GetValue() &&
          !proxy_->GetProfile()->IsOffTheRecord();
    return false;
  }
  
  bool SpellingMenuObserver::IsCommandIdEnabled(int command_id) {
    DCHECK(IsCommandIdSupported(command_id));
  
    if (command_id >= IDC_SPELLCHECK_SUGGESTION_0 &&
        command_id <= IDC_SPELLCHECK_SUGGESTION_LAST)
      return true;
  
    switch (command_id) {
      case IDC_SPELLCHECK_ADD_TO_DICTIONARY:
        return !misspelled_word_.empty();
  
      case IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS:
        return false;
  
      case IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION:
        return succeeded_;
  
      case IDC_CONTENT_CONTEXT_SPELLING_TOGGLE:
        return integrate_spelling_service_.IsUserModifiable() &&
            !proxy_->GetProfile()->IsOffTheRecord();
  
      case IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE:
        return integrate_spelling_service_.IsUserModifiable() &&
            !proxy_->GetProfile()->IsOffTheRecord();
  
      default:
        return false;
    }
    return false;
  }
  
  void SpellingMenuObserver::ExecuteCommand(int command_id) {
    DCHECK(IsCommandIdSupported(command_id));
  
    if (command_id >= IDC_SPELLCHECK_SUGGESTION_0 &&
        command_id <= IDC_SPELLCHECK_SUGGESTION_LAST) {
      int suggestion_index = command_id - IDC_SPELLCHECK_SUGGESTION_0;
      proxy_->GetRenderViewHost()->ReplaceMisspelling(
          suggestions_[suggestion_index]);
      // GetSpellCheckHost() can return null when the suggested word is provided
      // by Web SpellCheck API.
      Profile* profile = proxy_->GetProfile();
      if (profile) {
        SpellcheckService* spellcheck =
            SpellcheckServiceFactory::GetForContext(profile);
        if (spellcheck) {
          if (spellcheck->GetMetrics())
            spellcheck->GetMetrics()->RecordReplacedWordStats(1);
          spellcheck->GetFeedbackSender()->SelectedSuggestion(
              misspelling_hash_, suggestion_index);
        }
      }
      return;
    }
  
    // When we choose the suggestion sent from the Spelling service, we replace
    // the misspelled word with the suggestion and add it to our custom-word
    // dictionary so this word is not marked as misspelled any longer.
    if (command_id == IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION) {
      proxy_->GetRenderViewHost()->ReplaceMisspelling(result_);
      misspelled_word_ = result_;
    }
  
    if (command_id == IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION ||
        command_id == IDC_SPELLCHECK_ADD_TO_DICTIONARY) {
      // GetHostForProfile() can return null when the suggested word is provided
      // by Web SpellCheck API.
      Profile* profile = proxy_->GetProfile();
      if (profile) {
        SpellcheckService* spellcheck =
            SpellcheckServiceFactory::GetForContext(profile);
        if (spellcheck) {
          spellcheck->GetCustomDictionary()->AddWord(base::UTF16ToUTF8(
              misspelled_word_));
          spellcheck->GetFeedbackSender()->AddedToDictionary(misspelling_hash_);
        }
      }
  #if defined(OS_MACOSX)
      spellcheck_mac::AddWord(misspelled_word_);
  #endif
    }
  
    // The spelling service can be toggled by the user only if it is not managed.
    if (command_id == IDC_CONTENT_CONTEXT_SPELLING_TOGGLE &&
        integrate_spelling_service_.IsUserModifiable()) {
      // When a user enables the "Ask Google for spelling suggestions" item, we
      // show a bubble to confirm it. On the other hand, when a user disables this
      // item, we directly update/ the profile and stop integrating the spelling
      // service immediately.
      if (!integrate_spelling_service_.GetValue()) {
        content::RenderViewHost* rvh = proxy_->GetRenderViewHost();
        gfx::Rect rect = rvh->GetView()->GetViewBounds();
        chrome::ShowConfirmBubble(
  #if defined(TOOLKIT_VIEWS)
            proxy_->GetWebContents()->GetView()->GetTopLevelNativeWindow(),
  #else
            rvh->GetView()->GetNativeView(),
  #endif
            gfx::Point(rect.CenterPoint().x(), rect.y()),
            new SpellingBubbleModel(proxy_->GetProfile(),
                                    proxy_->GetWebContents(),
                                    false));
      } else {
        Profile* profile = proxy_->GetProfile();
        if (profile)
          profile->GetPrefs()->SetBoolean(prefs::kSpellCheckUseSpellingService,
                                          false);
          profile->GetPrefs()->SetBoolean(prefs::kEnableAutoSpellCorrect,
                                          false);
      }
    }
    // Autocorrect requires use of the spelling service and the spelling service
    // can be toggled by the user only if it is not managed.
    if (command_id == IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE &&
        integrate_spelling_service_.IsUserModifiable()) {
      // When the user enables autocorrect, we'll need to make sure that we can
      // ask Google for suggestions since that service is required. So we show
      // the bubble and just make sure to enable autocorrect as well.
      if (!integrate_spelling_service_.GetValue()) {
        content::RenderViewHost* rvh = proxy_->GetRenderViewHost();
        gfx::Rect rect = rvh->GetView()->GetViewBounds();
        chrome::ShowConfirmBubble(rvh->GetView()->GetNativeView(),
                                  gfx::Point(rect.CenterPoint().x(), rect.y()),
                                  new SpellingBubbleModel(
                                      proxy_->GetProfile(),
                                      proxy_->GetWebContents(),
                                      true));
      } else {
        Profile* profile = proxy_->GetProfile();
        if (profile) {
          bool current_value = autocorrect_spelling_.GetValue();
          profile->GetPrefs()->SetBoolean(prefs::kEnableAutoSpellCorrect,
                                          !current_value);
        }
      }
    }
  }
  
  void SpellingMenuObserver::OnMenuCancel() {
    Profile* profile = proxy_->GetProfile();
    if (!profile)
      return;
    SpellcheckService* spellcheck =
        SpellcheckServiceFactory::GetForContext(profile);
    if (!spellcheck)
      return;
    spellcheck->GetFeedbackSender()->IgnoredSuggestions(misspelling_hash_);
  }
  
  void SpellingMenuObserver::OnTextCheckComplete(
      SpellingServiceClient::ServiceType type,
      bool success,
      const base::string16& text,
      const std::vector<SpellCheckResult>& results) {
    animation_timer_.Stop();
  
    // Scan the text-check results and replace the misspelled regions with
    // suggested words. If the replaced text is included in the suggestion list
    // provided by the local spellchecker, we show a "No suggestions from Google"
    // message.
    succeeded_ = success;
    if (results.empty()) {
      succeeded_ = false;
    } else {
      typedef std::vector<SpellCheckResult> SpellCheckResults;
      for (SpellCheckResults::const_iterator it = results.begin();
           it != results.end(); ++it) {
        result_.replace(it->location, it->length, it->replacement);
      }
      base::string16 result = base::i18n::ToLower(result_);
      for (std::vector<base::string16>::const_iterator it = suggestions_.begin();
           it != suggestions_.end(); ++it) {
        if (result == base::i18n::ToLower(*it)) {
          succeeded_ = false;
          break;
        }
      }
    }
    if (type != SpellingServiceClient::SPELLCHECK) {
      if (!succeeded_) {
        result_ = l10n_util::GetStringUTF16(
            IDS_CONTENT_CONTEXT_SPELLING_NO_SUGGESTIONS_FROM_GOOGLE);
      }
  
      // Update the menu item with the result text. We disable this item and hide
      // it when the spelling service does not provide valid suggestions.
      proxy_->UpdateMenuItem(IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION, succeeded_,
                             false, result_);
    }
  }
  
  void SpellingMenuObserver::OnAnimationTimerExpired() {
    // Append '.' characters to the end of "Checking".
    loading_frame_ = (loading_frame_ + 1) & 3;
    base::string16 loading_message =
        loading_message_ + base::string16(loading_frame_,'.');
  
    // Update the menu item with the text. We disable this item to prevent users
    // from selecting it.
    proxy_->UpdateMenuItem(IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION, false, false,
                           loading_message);
  }
  