| // 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. |
| |
| #include "chrome/renderer/spellchecker/spellcheck_provider.h" |
| |
| #include "base/command_line.h" |
| #include "base/metrics/histogram.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "chrome/common/spellcheck_marker.h" |
| #include "chrome/common/spellcheck_messages.h" |
| #include "chrome/common/spellcheck_result.h" |
| #include "chrome/renderer/spellchecker/spellcheck.h" |
| #include "content/public/renderer/render_view.h" |
| #include "third_party/WebKit/public/platform/WebVector.h" |
| #include "third_party/WebKit/public/web/WebElement.h" |
| #include "third_party/WebKit/public/web/WebFrame.h" |
| #include "third_party/WebKit/public/web/WebTextCheckingCompletion.h" |
| #include "third_party/WebKit/public/web/WebTextCheckingResult.h" |
| #include "third_party/WebKit/public/web/WebTextDecorationType.h" |
| #include "third_party/WebKit/public/web/WebView.h" |
| |
| using blink::WebFrame; |
| using blink::WebString; |
| using blink::WebTextCheckingCompletion; |
| using blink::WebTextCheckingResult; |
| using blink::WebTextDecorationType; |
| using blink::WebVector; |
| |
| COMPILE_ASSERT(int(blink::WebTextDecorationTypeSpelling) == |
| int(SpellCheckResult::SPELLING), mismatching_enums); |
| COMPILE_ASSERT(int(blink::WebTextDecorationTypeGrammar) == |
| int(SpellCheckResult::GRAMMAR), mismatching_enums); |
| COMPILE_ASSERT(int(blink::WebTextDecorationTypeInvisibleSpellcheck) == |
| int(SpellCheckResult::INVISIBLE), mismatching_enums); |
| |
| SpellCheckProvider::SpellCheckProvider( |
| content::RenderView* render_view, |
| SpellCheck* spellcheck) |
| : content::RenderViewObserver(render_view), |
| content::RenderViewObserverTracker<SpellCheckProvider>(render_view), |
| spelling_panel_visible_(false), |
| spellcheck_(spellcheck) { |
| DCHECK(spellcheck_); |
| if (render_view) { // NULL in unit tests. |
| render_view->GetWebView()->setSpellCheckClient(this); |
| EnableSpellcheck(spellcheck_->is_spellcheck_enabled()); |
| } |
| } |
| |
| SpellCheckProvider::~SpellCheckProvider() { |
| } |
| |
| void SpellCheckProvider::RequestTextChecking( |
| const base::string16& text, |
| WebTextCheckingCompletion* completion, |
| const std::vector<SpellCheckMarker>& markers) { |
| // Ignore invalid requests. |
| if (text.empty() || !HasWordCharacters(text, 0)) { |
| completion->didCancelCheckingText(); |
| return; |
| } |
| |
| // Try to satisfy check from cache. |
| if (SatisfyRequestFromCache(text, completion)) |
| return; |
| |
| // Send this text to a browser. A browser checks the user profile and send |
| // this text to the Spelling service only if a user enables this feature. |
| last_request_.clear(); |
| last_results_.assign(blink::WebVector<blink::WebTextCheckingResult>()); |
| |
| #if defined(OS_MACOSX) |
| // Text check (unified request for grammar and spell check) is only |
| // available for browser process, so we ask the system spellchecker |
| // over IPC or return an empty result if the checker is not |
| // available. |
| Send(new SpellCheckHostMsg_RequestTextCheck( |
| routing_id(), |
| text_check_completions_.Add(completion), |
| text, |
| markers)); |
| #else |
| Send(new SpellCheckHostMsg_CallSpellingService( |
| routing_id(), |
| text_check_completions_.Add(completion), |
| base::string16(text), |
| markers)); |
| #endif // !OS_MACOSX |
| } |
| |
| bool SpellCheckProvider::OnMessageReceived(const IPC::Message& message) { |
| bool handled = true; |
| IPC_BEGIN_MESSAGE_MAP(SpellCheckProvider, message) |
| #if !defined(OS_MACOSX) |
| IPC_MESSAGE_HANDLER(SpellCheckMsg_RespondSpellingService, |
| OnRespondSpellingService) |
| #endif |
| #if defined(OS_MACOSX) |
| IPC_MESSAGE_HANDLER(SpellCheckMsg_AdvanceToNextMisspelling, |
| OnAdvanceToNextMisspelling) |
| IPC_MESSAGE_HANDLER(SpellCheckMsg_RespondTextCheck, OnRespondTextCheck) |
| IPC_MESSAGE_HANDLER(SpellCheckMsg_ToggleSpellPanel, OnToggleSpellPanel) |
| #endif |
| IPC_MESSAGE_UNHANDLED(handled = false) |
| IPC_END_MESSAGE_MAP() |
| return handled; |
| } |
| |
| void SpellCheckProvider::FocusedNodeChanged(const blink::WebNode& unused) { |
| #if defined(OS_MACOSX) |
| bool enabled = false; |
| blink::WebElement element = render_view()->GetFocusedElement(); |
| if (!element.isNull()) |
| enabled = render_view()->IsEditableNode(element); |
| |
| bool checked = false; |
| if (enabled && render_view()->GetWebView()) { |
| WebFrame* frame = render_view()->GetWebView()->focusedFrame(); |
| if (frame->isContinuousSpellCheckingEnabled()) |
| checked = true; |
| } |
| |
| Send(new SpellCheckHostMsg_ToggleSpellCheck(routing_id(), enabled, checked)); |
| #endif // OS_MACOSX |
| } |
| |
| void SpellCheckProvider::spellCheck( |
| const WebString& text, |
| int& offset, |
| int& length, |
| WebVector<WebString>* optional_suggestions) { |
| base::string16 word(text); |
| std::vector<base::string16> suggestions; |
| spellcheck_->SpellCheckWord( |
| word.c_str(), word.size(), routing_id(), |
| &offset, &length, optional_suggestions ? & suggestions : NULL); |
| if (optional_suggestions) { |
| *optional_suggestions = suggestions; |
| UMA_HISTOGRAM_COUNTS("SpellCheck.api.check.suggestions", word.size()); |
| } else { |
| UMA_HISTOGRAM_COUNTS("SpellCheck.api.check", word.size()); |
| // If optional_suggestions is not requested, the API is called |
| // for marking. So we use this for counting markable words. |
| Send(new SpellCheckHostMsg_NotifyChecked(routing_id(), word, 0 < length)); |
| } |
| } |
| |
| void SpellCheckProvider::checkTextOfParagraph( |
| const blink::WebString& text, |
| blink::WebTextCheckingTypeMask mask, |
| blink::WebVector<blink::WebTextCheckingResult>* results) { |
| if (!results) |
| return; |
| |
| if (!(mask & blink::WebTextCheckingTypeSpelling)) |
| return; |
| |
| // TODO(groby): As far as I can tell, this method is never invoked. |
| // UMA results seem to support that. Investigate, clean up if true. |
| NOTREACHED(); |
| spellcheck_->SpellCheckParagraph(text, results); |
| UMA_HISTOGRAM_COUNTS("SpellCheck.api.paragraph", text.length()); |
| } |
| |
| void SpellCheckProvider::requestCheckingOfText( |
| const WebString& text, |
| const WebVector<uint32>& markers, |
| const WebVector<unsigned>& marker_offsets, |
| WebTextCheckingCompletion* completion) { |
| std::vector<SpellCheckMarker> spellcheck_markers; |
| for (size_t i = 0; i < markers.size(); ++i) { |
| spellcheck_markers.push_back( |
| SpellCheckMarker(markers[i], marker_offsets[i])); |
| } |
| RequestTextChecking(text, completion, spellcheck_markers); |
| UMA_HISTOGRAM_COUNTS("SpellCheck.api.async", text.length()); |
| } |
| |
| WebString SpellCheckProvider::autoCorrectWord(const WebString& word) { |
| const CommandLine& command_line = *CommandLine::ForCurrentProcess(); |
| if (command_line.HasSwitch(switches::kEnableSpellingAutoCorrect)) { |
| UMA_HISTOGRAM_COUNTS("SpellCheck.api.autocorrect", word.length()); |
| return spellcheck_->GetAutoCorrectionWord(word, routing_id()); |
| } |
| return base::string16(); |
| } |
| |
| void SpellCheckProvider::showSpellingUI(bool show) { |
| #if defined(OS_MACOSX) |
| UMA_HISTOGRAM_BOOLEAN("SpellCheck.api.showUI", show); |
| Send(new SpellCheckHostMsg_ShowSpellingPanel(routing_id(), show)); |
| #endif |
| } |
| |
| bool SpellCheckProvider::isShowingSpellingUI() { |
| return spelling_panel_visible_; |
| } |
| |
| void SpellCheckProvider::updateSpellingUIWithMisspelledWord( |
| const WebString& word) { |
| #if defined(OS_MACOSX) |
| Send(new SpellCheckHostMsg_UpdateSpellingPanelWithMisspelledWord(routing_id(), |
| word)); |
| #endif |
| } |
| |
| #if !defined(OS_MACOSX) |
| void SpellCheckProvider::OnRespondSpellingService( |
| int identifier, |
| bool succeeded, |
| const base::string16& line, |
| const std::vector<SpellCheckResult>& results) { |
| WebTextCheckingCompletion* completion = |
| text_check_completions_.Lookup(identifier); |
| if (!completion) |
| return; |
| text_check_completions_.Remove(identifier); |
| |
| // If |succeeded| is false, we use local spellcheck as a fallback. |
| if (!succeeded) { |
| spellcheck_->RequestTextChecking(line, completion); |
| return; |
| } |
| |
| // Double-check the returned spellchecking results with our spellchecker to |
| // visualize the differences between ours and the on-line spellchecker. |
| blink::WebVector<blink::WebTextCheckingResult> textcheck_results; |
| spellcheck_->CreateTextCheckingResults(SpellCheck::USE_NATIVE_CHECKER, |
| 0, |
| line, |
| results, |
| &textcheck_results); |
| completion->didFinishCheckingText(textcheck_results); |
| |
| // Cache the request and the converted results. |
| last_request_ = line; |
| last_results_.swap(textcheck_results); |
| } |
| #endif |
| |
| bool SpellCheckProvider::HasWordCharacters( |
| const base::string16& text, |
| int index) const { |
| const base::char16* data = text.data(); |
| int length = text.length(); |
| while (index < length) { |
| uint32 code = 0; |
| U16_NEXT(data, index, length, code); |
| UErrorCode error = U_ZERO_ERROR; |
| if (uscript_getScript(code, &error) != USCRIPT_COMMON) |
| return true; |
| } |
| return false; |
| } |
| |
| #if defined(OS_MACOSX) |
| void SpellCheckProvider::OnAdvanceToNextMisspelling() { |
| if (!render_view()->GetWebView()) |
| return; |
| render_view()->GetWebView()->focusedFrame()->executeCommand( |
| WebString::fromUTF8("AdvanceToNextMisspelling")); |
| } |
| |
| void SpellCheckProvider::OnRespondTextCheck( |
| int identifier, |
| const std::vector<SpellCheckResult>& results) { |
| // TODO(groby): Unify with SpellCheckProvider::OnRespondSpellingService |
| DCHECK(spellcheck_); |
| WebTextCheckingCompletion* completion = |
| text_check_completions_.Lookup(identifier); |
| if (!completion) |
| return; |
| text_check_completions_.Remove(identifier); |
| blink::WebVector<blink::WebTextCheckingResult> textcheck_results; |
| spellcheck_->CreateTextCheckingResults(SpellCheck::DO_NOT_MODIFY, |
| 0, |
| base::string16(), |
| results, |
| &textcheck_results); |
| completion->didFinishCheckingText(textcheck_results); |
| |
| // TODO(groby): Add request caching once OSX reports back original request. |
| // (cf. SpellCheckProvider::OnRespondSpellingService) |
| // Cache the request and the converted results. |
| } |
| |
| void SpellCheckProvider::OnToggleSpellPanel(bool is_currently_visible) { |
| if (!render_view()->GetWebView()) |
| return; |
| // We need to tell the webView whether the spelling panel is visible or not so |
| // that it won't need to make ipc calls later. |
| spelling_panel_visible_ = is_currently_visible; |
| render_view()->GetWebView()->focusedFrame()->executeCommand( |
| WebString::fromUTF8("ToggleSpellPanel")); |
| } |
| #endif |
| |
| void SpellCheckProvider::EnableSpellcheck(bool enable) { |
| if (!render_view()->GetWebView()) |
| return; |
| |
| WebFrame* frame = render_view()->GetWebView()->focusedFrame(); |
| frame->enableContinuousSpellChecking(enable); |
| if (!enable) |
| frame->removeSpellingMarkers(); |
| } |
| |
| bool SpellCheckProvider::SatisfyRequestFromCache( |
| const base::string16& text, |
| WebTextCheckingCompletion* completion) { |
| size_t last_length = last_request_.length(); |
| |
| // Send back the |last_results_| if the |last_request_| is a substring of |
| // |text| and |text| does not have more words to check. Provider cannot cancel |
| // the spellcheck request here, because WebKit might have discarded the |
| // previous spellcheck results and erased the spelling markers in response to |
| // the user editing the text. |
| base::string16 request(text); |
| size_t text_length = request.length(); |
| if (text_length >= last_length && |
| !request.compare(0, last_length, last_request_)) { |
| if (text_length == last_length || !HasWordCharacters(text, last_length)) { |
| completion->didFinishCheckingText(last_results_); |
| return true; |
| } |
| int code = 0; |
| int length = static_cast<int>(text_length); |
| U16_PREV(text.data(), 0, length, code); |
| UErrorCode error = U_ZERO_ERROR; |
| if (uscript_getScript(code, &error) != USCRIPT_COMMON) { |
| completion->didCancelCheckingText(); |
| return true; |
| } |
| } |
| // Create a subset of the cached results and return it if the given text is a |
| // substring of the cached text. |
| if (text_length < last_length && |
| !last_request_.compare(0, text_length, request)) { |
| size_t result_size = 0; |
| for (size_t i = 0; i < last_results_.size(); ++i) { |
| size_t start = last_results_[i].location; |
| size_t end = start + last_results_[i].length; |
| if (start <= text_length && end <= text_length) |
| ++result_size; |
| } |
| if (result_size > 0) { |
| blink::WebVector<blink::WebTextCheckingResult> results(result_size); |
| for (size_t i = 0; i < result_size; ++i) { |
| results[i].decoration = last_results_[i].decoration; |
| results[i].location = last_results_[i].location; |
| results[i].length = last_results_[i].length; |
| results[i].replacement = last_results_[i].replacement; |
| } |
| completion->didFinishCheckingText(results); |
| return true; |
| } |
| } |
| |
| return false; |
| } |