| // Copyright 2010-2014, Google Inc. |
| // All rights reserved. |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are |
| // met: |
| // |
| // * Redistributions of source code must retain the above copyright |
| // notice, this list of conditions and the following disclaimer. |
| // * Redistributions in binary form must reproduce the above |
| // copyright notice, this list of conditions and the following disclaimer |
| // in the documentation and/or other materials provided with the |
| // distribution. |
| // * Neither the name of Google Inc. nor the names of its |
| // contributors may be used to endorse or promote products derived from |
| // this software without specific prior written permission. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| // A class handling the converter on the session layer. |
| |
| #include "session/session_converter.h" |
| |
| #include <algorithm> |
| #include <limits> |
| #include <string> |
| |
| #include "base/logging.h" |
| #include "base/port.h" |
| #include "base/text_normalizer.h" |
| #include "base/util.h" |
| #include "composer/composer.h" |
| #include "config/config.pb.h" |
| #include "config/config_handler.h" |
| #include "converter/converter_interface.h" |
| #include "converter/converter_util.h" |
| #include "converter/segments.h" |
| #include "session/commands.pb.h" |
| #include "session/internal/candidate_list.h" |
| #include "session/internal/session_output.h" |
| #include "session/session_usage_stats_util.h" |
| #include "transliteration/transliteration.h" |
| #include "usage_stats/usage_stats.h" |
| |
| using mozc::usage_stats::UsageStats; |
| |
| #ifdef OS_ANDROID |
| const bool kDefaultUseActualConverterForRealtimeConversion = false; |
| #else |
| const bool kDefaultUseActualConverterForRealtimeConversion = true; |
| #endif // OS_ANDROID |
| |
| DEFINE_bool(use_actual_converter_for_realtime_conversion, |
| kDefaultUseActualConverterForRealtimeConversion, |
| "If true, use the actual (non-immutable) converter for real " |
| "time conversion."); |
| |
| namespace mozc { |
| namespace session { |
| |
| namespace { |
| |
| using mozc::commands::Request; |
| using mozc::config::Config; |
| using mozc::config::ConfigHandler; |
| |
| const size_t kDefaultMaxHistorySize = 3; |
| |
| void SetPresentationMode(bool enabled) { |
| Config config; |
| ConfigHandler::GetConfig(&config); |
| config.set_presentation_mode(enabled); |
| ConfigHandler::SetConfig(config); |
| } |
| |
| void SetIncognitoMode(bool enabled) { |
| Config config; |
| ConfigHandler::GetConfig(&config); |
| config.set_incognito_mode(enabled); |
| ConfigHandler::SetConfig(config); |
| } |
| |
| } // namespace |
| |
| const size_t SessionConverter::kConsumedAllCharacters = |
| numeric_limits<size_t>::max(); |
| |
| SessionConverter::SessionConverter(const ConverterInterface *converter, |
| const Request *request) |
| : SessionConverterInterface(), |
| state_(COMPOSITION), |
| converter_(converter), |
| segments_(new Segments), |
| segment_index_(0), |
| result_(new commands::Result), |
| candidate_list_(new CandidateList(true)), |
| candidate_list_visible_(false), |
| request_(request), |
| client_revision_(0) { |
| conversion_preferences_.use_history = true; |
| conversion_preferences_.max_history_size = kDefaultMaxHistorySize; |
| conversion_preferences_.request_suggestion = true; |
| operation_preferences_.use_cascading_window = true; |
| operation_preferences_.candidate_shortcuts.clear(); |
| } |
| |
| SessionConverter::~SessionConverter() {} |
| |
| void SessionConverter::SetOperationPreferences( |
| const OperationPreferences &preferences) { |
| operation_preferences_.use_cascading_window = |
| preferences.use_cascading_window; |
| operation_preferences_.candidate_shortcuts = |
| preferences.candidate_shortcuts; |
| } |
| |
| bool SessionConverter::CheckState( |
| SessionConverterInterface::States states) const { |
| return ((state_ & states) != NO_STATE); |
| } |
| |
| bool SessionConverter::IsActive() const { |
| return CheckState(SUGGESTION | PREDICTION | CONVERSION); |
| } |
| |
| const ConversionPreferences &SessionConverter::conversion_preferences() const { |
| return conversion_preferences_; |
| } |
| |
| bool SessionConverter::Convert(const composer::Composer &composer) { |
| return ConvertWithPreferences(composer, conversion_preferences_); |
| } |
| |
| bool SessionConverter::ConvertWithPreferences( |
| const composer::Composer &composer, |
| const ConversionPreferences &preferences) { |
| DCHECK(CheckState(COMPOSITION | SUGGESTION | CONVERSION)); |
| |
| segments_->set_request_type(Segments::CONVERSION); |
| SetConversionPreferences(preferences, segments_.get()); |
| |
| const ConversionRequest conversion_request(&composer, request_); |
| if (!converter_->StartConversionForRequest(conversion_request, |
| segments_.get())) { |
| LOG(WARNING) << "StartConversionForRequest() failed"; |
| ResetState(); |
| return false; |
| } |
| |
| segment_index_ = 0; |
| state_ = CONVERSION; |
| candidate_list_visible_ = false; |
| UpdateCandidateList(); |
| InitializeSelectedCandidateIndices(); |
| return true; |
| } |
| |
| bool SessionConverter::GetReadingText(const string &source_text, |
| string *reading) { |
| DCHECK(reading); |
| reading->clear(); |
| Segments reverse_segments; |
| if (!converter_->StartReverseConversion(&reverse_segments, source_text)) { |
| return false; |
| } |
| if (reverse_segments.segments_size() == 0) { |
| LOG(WARNING) << "no segments from reverse conversion"; |
| return false; |
| } |
| for (size_t i = 0; i < reverse_segments.segments_size(); ++i) { |
| const mozc::Segment &segment = reverse_segments.segment(i); |
| if (segment.candidates_size() == 0) { |
| LOG(WARNING) << "got an empty segment from reverse conversion"; |
| return false; |
| } |
| reading->append(segment.candidate(0).value); |
| } |
| return true; |
| } |
| |
| namespace { |
| Attributes GetT13nAttributes(const transliteration::TransliterationType type) { |
| Attributes attributes = NO_ATTRIBUTES; |
| switch (type) { |
| case transliteration::HIRAGANA: // "ひらがな" |
| attributes = HIRAGANA; |
| break; |
| case transliteration::FULL_KATAKANA: // "カタカナ" |
| attributes = (FULL_WIDTH | KATAKANA); |
| break; |
| case transliteration::HALF_ASCII: // "ascII" |
| attributes = (HALF_WIDTH | ASCII); |
| break; |
| case transliteration::HALF_ASCII_UPPER: // "ASCII" |
| attributes = (HALF_WIDTH | ASCII | UPPER); |
| break; |
| case transliteration::HALF_ASCII_LOWER: // "ascii" |
| attributes = (HALF_WIDTH | ASCII | LOWER); |
| break; |
| case transliteration::HALF_ASCII_CAPITALIZED: // "Ascii" |
| attributes = (HALF_WIDTH | ASCII | CAPITALIZED); |
| break; |
| case transliteration::FULL_ASCII: // "ascII" |
| attributes = (FULL_WIDTH | ASCII); |
| break; |
| case transliteration::FULL_ASCII_UPPER: // "ASCII" |
| attributes = (FULL_WIDTH | ASCII | UPPER); |
| break; |
| case transliteration::FULL_ASCII_LOWER: // "ascii" |
| attributes = (FULL_WIDTH | ASCII | LOWER); |
| break; |
| case transliteration::FULL_ASCII_CAPITALIZED: // "Ascii" |
| attributes = (FULL_WIDTH | ASCII | CAPITALIZED); |
| break; |
| case transliteration::HALF_KATAKANA: // "カタカナ" |
| attributes = (HALF_WIDTH | KATAKANA); |
| break; |
| default: |
| LOG(ERROR) << "Unknown type: " << type; |
| break; |
| } |
| return attributes; |
| } |
| } // namespace |
| |
| bool SessionConverter::ConvertToTransliteration( |
| const composer::Composer &composer, |
| const transliteration::TransliterationType type) { |
| DCHECK(CheckState(COMPOSITION | SUGGESTION | PREDICTION | CONVERSION)); |
| if (CheckState(PREDICTION)) { |
| // TODO(komatsu): A better way is to transliterate the key of the |
| // focused candidate. However it takes a long time. |
| Cancel(); |
| DCHECK(CheckState(COMPOSITION)); |
| } |
| |
| Attributes query_attr = |
| (GetT13nAttributes(type) & |
| (HALF_WIDTH | FULL_WIDTH | ASCII | HIRAGANA | KATAKANA)); |
| |
| if (CheckState(COMPOSITION | SUGGESTION)) { |
| if (!Convert(composer)) { |
| LOG(ERROR) << "Conversion failed"; |
| return false; |
| } |
| |
| // TODO(komatsu): This is a workaround to transliterate the whole |
| // preedit as a single segment. We should modify |
| // converter/converter.cc to enable to accept mozc::Segment::FIXED |
| // from the session layer. |
| if (segments_->conversion_segments_size() != 1) { |
| string composition; |
| GetPreedit(0, segments_->conversion_segments_size(), &composition); |
| ResizeSegmentWidth(composer, Util::CharsLen(composition)); |
| } |
| |
| DCHECK(CheckState(CONVERSION)); |
| candidate_list_->MoveToAttributes(query_attr); |
| } else { |
| DCHECK(CheckState(CONVERSION)); |
| const Attributes current_attr = |
| candidate_list_->GetDeepestFocusedCandidate().attributes(); |
| |
| if ((query_attr & current_attr & ASCII) && |
| ((((query_attr & HALF_WIDTH) && (current_attr & FULL_WIDTH))) || |
| (((query_attr & FULL_WIDTH) && (current_attr & HALF_WIDTH))))) { |
| query_attr |= (current_attr & (UPPER | LOWER | CAPITALIZED)); |
| } |
| |
| candidate_list_->MoveNextAttributes(query_attr); |
| } |
| candidate_list_visible_ = false; |
| // Treat as top conversion candidate on usage stats. |
| selected_candidate_indices_[segment_index_] = 0; |
| SegmentFocus(); |
| return true; |
| } |
| |
| bool SessionConverter::ConvertToHalfWidth(const composer::Composer &composer) { |
| DCHECK(CheckState(COMPOSITION | SUGGESTION | PREDICTION | CONVERSION)); |
| if (CheckState(PREDICTION)) { |
| // TODO(komatsu): A better way is to transliterate the key of the |
| // focused candidate. However it takes a long time. |
| Cancel(); |
| DCHECK(CheckState(COMPOSITION)); |
| } |
| |
| string composition; |
| if (CheckState(COMPOSITION | SUGGESTION)) { |
| composer.GetStringForPreedit(&composition); |
| } else { |
| composition = GetSelectedCandidate(segment_index_).value; |
| } |
| |
| // TODO(komatsu): make a function to return a logical sum of ScriptType. |
| // If composition_ is "あbc", it should be treated as Katakana. |
| if (Util::ContainsScriptType(composition, Util::KATAKANA) || |
| Util::ContainsScriptType(composition, Util::HIRAGANA) || |
| Util::ContainsScriptType(composition, Util::KANJI) || |
| Util::IsKanaSymbolContained(composition)) { |
| return ConvertToTransliteration(composer, transliteration::HALF_KATAKANA); |
| } else { |
| return ConvertToTransliteration(composer, transliteration::HALF_ASCII); |
| } |
| } |
| |
| bool SessionConverter::SwitchKanaType(const composer::Composer &composer) { |
| DCHECK(CheckState(COMPOSITION | SUGGESTION | PREDICTION | CONVERSION)); |
| if (CheckState(PREDICTION)) { |
| // TODO(komatsu): A better way is to transliterate the key of the |
| // focused candidate. However it takes a long time. |
| Cancel(); |
| DCHECK(CheckState(COMPOSITION)); |
| } |
| |
| Attributes attributes = NO_ATTRIBUTES; |
| if (CheckState(COMPOSITION | SUGGESTION)) { |
| if (!Convert(composer)) { |
| LOG(ERROR) << "Conversion failed"; |
| return false; |
| } |
| |
| // TODO(komatsu): This is a workaround to transliterate the whole |
| // preedit as a single segment. We should modify |
| // converter/converter.cc to enable to accept mozc::Segment::FIXED |
| // from the session layer. |
| if (segments_->conversion_segments_size() != 1) { |
| string composition; |
| GetPreedit(0, segments_->conversion_segments_size(), &composition); |
| const ConversionRequest conversion_request(&composer, request_); |
| converter_->ResizeSegment(segments_.get(), |
| conversion_request, |
| 0, Util::CharsLen(composition)); |
| UpdateCandidateList(); |
| } |
| |
| attributes = (FULL_WIDTH | KATAKANA); |
| } else { |
| const Attributes current_attributes = |
| candidate_list_->GetDeepestFocusedCandidate().attributes(); |
| // "漢字" -> "かんじ" -> "カンジ" -> "カンジ" -> "かんじ" -> ... |
| if (current_attributes & HIRAGANA) { |
| attributes = (FULL_WIDTH | KATAKANA); |
| } else if ((current_attributes & KATAKANA) && |
| (current_attributes & FULL_WIDTH)) { |
| attributes = (HALF_WIDTH | KATAKANA); |
| } else { |
| attributes = HIRAGANA; |
| } |
| } |
| |
| DCHECK(CheckState(CONVERSION)); |
| candidate_list_->MoveNextAttributes(attributes); |
| candidate_list_visible_ = false; |
| // Treat as top conversion candidate on usage stats. |
| selected_candidate_indices_[segment_index_] = 0; |
| SegmentFocus(); |
| return true; |
| } |
| |
| namespace { |
| |
| // Prepend the candidates to the first conversion segment. |
| void PrependCandidates(const Segment &previous_segment, |
| const string &preedit, |
| Segments *segments) { |
| DCHECK(segments); |
| |
| // TODO(taku) want to have a method in converter to make an empty segment |
| if (segments->conversion_segments_size() == 0) { |
| segments->clear_conversion_segments(); |
| Segment *segment = segments->add_segment(); |
| segment->Clear(); |
| segment->set_key(preedit); |
| } |
| |
| DCHECK_EQ(1, segments->conversion_segments_size()); |
| Segment *segment = segments->mutable_conversion_segment(0); |
| DCHECK(segment); |
| |
| const size_t cands_size = previous_segment.candidates_size(); |
| for (size_t i = 0; i < cands_size; ++i) { |
| Segment::Candidate *candidate = segment->push_front_candidate(); |
| candidate->CopyFrom(previous_segment.candidate(cands_size - i - 1)); |
| } |
| *(segment->mutable_meta_candidates()) = previous_segment.meta_candidates(); |
| } |
| } // namespace |
| |
| |
| bool SessionConverter::Suggest(const composer::Composer &composer) { |
| return SuggestWithPreferences(composer, conversion_preferences_); |
| } |
| |
| bool SessionConverter::SuggestWithPreferences( |
| const composer::Composer &composer, |
| const ConversionPreferences &preferences) { |
| DCHECK(CheckState(COMPOSITION | SUGGESTION)); |
| candidate_list_visible_ = false; |
| |
| // Normalize the current state by resetting the previous state. |
| ResetState(); |
| |
| // If we are on a password field, suppress suggestion. |
| if (!preferences.request_suggestion || |
| composer.GetInputFieldType() == commands::Context::PASSWORD) { |
| return false; |
| } |
| |
| // Initialize the segments for suggestion. |
| SetConversionPreferences(preferences, segments_.get()); |
| |
| ConversionRequest conversion_request(&composer, request_); |
| const size_t cursor = composer.GetCursor(); |
| if (cursor == composer.GetLength() || cursor == 0 || |
| !request_->mixed_conversion()) { |
| conversion_request.set_create_partial_candidates( |
| request_->auto_partial_suggestion()); |
| conversion_request.set_use_actual_converter_for_realtime_conversion( |
| FLAGS_use_actual_converter_for_realtime_conversion); |
| if (!converter_->StartSuggestionForRequest(conversion_request, |
| segments_.get())) { |
| // TODO(komatsu): Because suggestion is a prefix search, once |
| // StartSuggestion returns false, this GetSuggestion always |
| // returns false. Refactor it. |
| VLOG(1) << "StartSuggestionForRequest() returns no suggestions."; |
| // Clear segments and keep the context |
| converter_->CancelConversion(segments_.get()); |
| return false; |
| } |
| } else { |
| // create_partial_candidates is false because auto partial suggestion |
| // should be activated only when the cursor is at the tail or head from |
| // the view point of UX. |
| // use_actual_converter_for_realtime_conversion is also false because of |
| // implementation reason. If the flag is true, all the composition |
| // characters will be used in the below process, which conflicts |
| // with *partial* prediction. |
| if (!converter_->StartPartialSuggestionForRequest(conversion_request, |
| segments_.get())) { |
| VLOG(1) << "StartPartialSuggestionForRequest() returns no suggestions."; |
| // Clear segments and keep the context |
| converter_->CancelConversion(segments_.get()); |
| return false; |
| } |
| } |
| DCHECK_EQ(1, segments_->conversion_segments_size()); |
| |
| // Copy current suggestions so that we can merge |
| // prediction/suggestions later |
| previous_suggestions_.CopyFrom(segments_->conversion_segment(0)); |
| |
| // TODO(komatsu): the next line can be deleted. |
| segment_index_ = 0; |
| state_ = SUGGESTION; |
| UpdateCandidateList(); |
| candidate_list_visible_ = true; |
| InitializeSelectedCandidateIndices(); |
| return true; |
| } |
| |
| |
| bool SessionConverter::Predict(const composer::Composer &composer) { |
| return PredictWithPreferences(composer, conversion_preferences_); |
| } |
| |
| bool SessionConverter::IsEmptySegment(const Segment &segment) const { |
| return ((segment.candidates_size() == 0) && |
| (segment.meta_candidates_size() == 0)); |
| } |
| |
| bool SessionConverter::PredictWithPreferences( |
| const composer::Composer &composer, |
| const ConversionPreferences &preferences) { |
| // TODO(komatsu): DCHECK should be |
| // DCHECK(CheckState(COMPOSITION | SUGGESTION | PREDICTION)); |
| DCHECK(CheckState(COMPOSITION | SUGGESTION | CONVERSION | PREDICTION)); |
| ResetResult(); |
| |
| // Initialize the segments for prediction |
| segments_->set_request_type(Segments::PREDICTION); |
| SetConversionPreferences(preferences, segments_.get()); |
| |
| const bool predict_first = |
| !CheckState(PREDICTION) && IsEmptySegment(previous_suggestions_); |
| |
| const bool predict_expand = |
| (CheckState(PREDICTION) && |
| !IsEmptySegment(previous_suggestions_) && |
| candidate_list_->size() > 0 && |
| candidate_list_->focused() && |
| candidate_list_->focused_index() == candidate_list_->last_index()); |
| |
| segments_->clear_conversion_segments(); |
| |
| if (predict_expand || predict_first) { |
| ConversionRequest conversion_request(&composer, request_); |
| conversion_request.set_use_actual_converter_for_realtime_conversion( |
| FLAGS_use_actual_converter_for_realtime_conversion); |
| if (!converter_->StartPredictionForRequest(conversion_request, |
| segments_.get())) { |
| LOG(WARNING) << "StartPredictionForRequest() failed"; |
| |
| // TODO(komatsu): Perform refactoring after checking the stability test. |
| // |
| // If predict_expand is true, it means we have prevous_suggestions_. |
| // So we can use it as the result of this prediction. |
| if (predict_first) { |
| ResetState(); |
| return false; |
| } |
| } |
| } |
| |
| // Merge suggestions and prediction |
| string preedit; |
| composer.GetQueryForPrediction(&preedit); |
| PrependCandidates(previous_suggestions_, preedit, segments_.get()); |
| |
| segment_index_ = 0; |
| state_ = PREDICTION; |
| UpdateCandidateList(); |
| candidate_list_visible_ = true; |
| InitializeSelectedCandidateIndices(); |
| |
| return true; |
| } |
| |
| bool SessionConverter::ExpandSuggestion(const composer::Composer &composer) { |
| return ExpandSuggestionWithPreferences(composer, conversion_preferences_); |
| } |
| |
| bool SessionConverter::ExpandSuggestionWithPreferences( |
| const composer::Composer &composer, |
| const ConversionPreferences &preferences) { |
| DCHECK(CheckState(COMPOSITION | SUGGESTION | PREDICTION)); |
| if (CheckState(COMPOSITION)) { |
| // Client can send EXPAND_SUGGESTION command when on composition mode. |
| // In such case we do nothing. |
| VLOG(1) << "ExpandSuggestion does nothing on composition mode."; |
| return false; |
| } |
| |
| ResetResult(); |
| |
| // Expand suggestion. |
| // Current implementation is hacky. |
| // We want prediction candidates, |
| // but want to set candidates' category SUGGESTION. |
| // TODO(matsuzakit or yamaguchi): Refactor following lines, |
| // after implemention of partial conversion. |
| |
| // Initialize the segments for prediction. |
| SetConversionPreferences(preferences, segments_.get()); |
| |
| string preedit; |
| composer.GetQueryForPrediction(&preedit); |
| |
| // We do not need "segments_->clear_conversion_segments()". |
| // Without this statement we can add additional candidates into |
| // existing segments. |
| |
| ConversionRequest conversion_request(&composer, request_); |
| |
| const size_t cursor = composer.GetCursor(); |
| if (cursor == composer.GetLength() || cursor == 0 || |
| !request_->mixed_conversion()) { |
| conversion_request.set_create_partial_candidates( |
| request_->auto_partial_suggestion()); |
| conversion_request.set_use_actual_converter_for_realtime_conversion( |
| FLAGS_use_actual_converter_for_realtime_conversion); |
| // This is abuse of StartPrediction(). |
| // TODO(matsuzakit or yamaguchi): Add ExpandSuggestion method |
| // to Converter class. |
| if (!converter_->StartPredictionForRequest(conversion_request, |
| segments_.get())) { |
| LOG(WARNING) << "StartPredictionForRequest() failed"; |
| } |
| } else { |
| // c.f. SuggestWithPreferences for ConversionRequest flags. |
| if (!converter_->StartPartialPredictionForRequest(conversion_request, |
| segments_.get())) { |
| VLOG(1) << "StartPartialPredictionForRequest() returns no suggestions."; |
| // Clear segments and keep the context |
| converter_->CancelConversion(segments_.get()); |
| return false; |
| } |
| } |
| // Overwrite the request type to SUGGESTION. |
| // Without this logic, a candidate gets focused that is unexpected behavior. |
| segments_->set_request_type(Segments::SUGGESTION); |
| |
| // Merge suggestions and predictions. |
| PrependCandidates(previous_suggestions_, preedit, segments_.get()); |
| |
| segment_index_ = 0; |
| // Call AppendCandidateList instead of UpdateCandidateList because |
| // we want to keep existing candidates. |
| // As a result, ExpandSuggestionWithPreferences adds expanded suggestion |
| // candidates at the tail of existing candidates. |
| AppendCandidateList(); |
| candidate_list_visible_ = true; |
| return true; |
| } |
| |
| void SessionConverter::MaybeExpandPrediction( |
| const composer::Composer &composer) { |
| DCHECK(CheckState(PREDICTION | CONVERSION)); |
| |
| // Expand the current suggestions and fill with Prediction results. |
| if (!CheckState(PREDICTION) || |
| IsEmptySegment(previous_suggestions_) || |
| !candidate_list_->focused() || |
| candidate_list_->focused_index() != candidate_list_->last_index()) { |
| return; |
| } |
| |
| DCHECK(CheckState(PREDICTION)); |
| ResetResult(); |
| |
| const size_t previous_index = candidate_list_->focused_index(); |
| if (!PredictWithPreferences(composer, conversion_preferences_)) { |
| return; |
| } |
| |
| DCHECK_LT(previous_index, candidate_list_->size()); |
| candidate_list_->MoveToId(candidate_list_->candidate(previous_index).id()); |
| UpdateSelectedCandidateIndex(); |
| } |
| |
| void SessionConverter::Cancel() { |
| DCHECK(CheckState(PREDICTION | CONVERSION)); |
| ResetResult(); |
| |
| // Clear segments and keep the context |
| converter_->CancelConversion(segments_.get()); |
| ResetState(); |
| } |
| |
| void SessionConverter::Reset() { |
| DCHECK(CheckState(COMPOSITION | SUGGESTION | PREDICTION | CONVERSION)); |
| |
| // Even if composition mode, call ResetConversion |
| // in order to clear history segments. |
| converter_->ResetConversion(segments_.get()); |
| |
| if (CheckState(COMPOSITION)) { |
| return; |
| } |
| |
| ResetResult(); |
| // Reset segments (and its internal context) |
| ResetState(); |
| } |
| |
| void SessionConverter::Commit(const composer::Composer &composer, |
| const commands::Context &context) { |
| DCHECK(CheckState(PREDICTION | CONVERSION)); |
| ResetResult(); |
| |
| if (!UpdateResult(0, segments_->conversion_segments_size(), NULL)) { |
| Cancel(); |
| ResetState(); |
| return; |
| } |
| |
| for (size_t i = 0; i < segments_->conversion_segments_size(); ++i) { |
| converter_->CommitSegmentValue(segments_.get(), |
| i, |
| GetCandidateIndexForConverter(i)); |
| } |
| CommitUsageStats(state_, context); |
| ConversionRequest conversion_request(&composer, request_); |
| converter_->FinishConversion(conversion_request, segments_.get()); |
| ResetState(); |
| } |
| |
| bool SessionConverter::CommitSuggestionInternal( |
| const composer::Composer &composer, |
| const commands::Context &context, |
| size_t *consumed_key_size) { |
| DCHECK(consumed_key_size); |
| DCHECK(CheckState(SUGGESTION)); |
| ResetResult(); |
| string preedit; |
| composer.GetStringForPreedit(&preedit); |
| |
| if (!UpdateResult(0, segments_->conversion_segments_size(), |
| consumed_key_size)) { |
| // Do not need to call Cancel like Commit because the current |
| // state is SUGGESTION. |
| ResetState(); |
| return false; |
| } |
| |
| const size_t preedit_length = Util::CharsLen(preedit); |
| |
| // TODO(horo): When we will support hardware keyboard and introduce |
| // shift+enter keymap in Android, this if condition may be insufficient. |
| if (request_->zero_query_suggestion() && |
| *consumed_key_size < composer.GetLength()) { |
| // A candidate was chosen from partial suggestion. |
| converter_->CommitPartialSuggestionSegmentValue( |
| segments_.get(), |
| 0, |
| GetCandidateIndexForConverter(0), |
| Util::SubString(preedit, 0, *consumed_key_size), |
| Util::SubString(preedit, |
| *consumed_key_size, |
| preedit_length - *consumed_key_size)); |
| CommitUsageStats(SessionConverterInterface::SUGGESTION, context); |
| InitializeSelectedCandidateIndices(); |
| // One or more segments must exist because new segment is inserted |
| // just after the commited segment. |
| DCHECK_GT(segments_->conversion_segments_size(), 0); |
| } else { |
| // Not partial suggestion so let's reset the state. |
| converter_->CommitSegmentValue(segments_.get(), |
| 0, |
| GetCandidateIndexForConverter(0)); |
| CommitUsageStats(SessionConverterInterface::SUGGESTION, context); |
| ConversionRequest conversion_request(&composer, request_); |
| converter_->FinishConversion(conversion_request, segments_.get()); |
| DCHECK_EQ(0, segments_->conversion_segments_size()); |
| ResetState(); |
| } |
| return true; |
| } |
| |
| bool SessionConverter::CommitSuggestionByIndex( |
| const size_t index, |
| const composer::Composer &composer, |
| const commands::Context &context, |
| size_t *consumed_key_size) { |
| DCHECK(CheckState(SUGGESTION)); |
| if (index >= candidate_list_->size()) { |
| LOG(ERROR) << "index is out of the range: " << index; |
| return false; |
| } |
| candidate_list_->MoveToPageIndex(index); |
| UpdateSelectedCandidateIndex(); |
| return CommitSuggestionInternal(composer, context, consumed_key_size); |
| } |
| |
| bool SessionConverter::CommitSuggestionById( |
| const int id, |
| const composer::Composer &composer, |
| const commands::Context &context, |
| size_t *consumed_key_size) { |
| DCHECK(CheckState(SUGGESTION)); |
| if (!candidate_list_->MoveToId(id)) { |
| // Don't use CandidateMoveToId() method, which overwrites candidates. |
| // This is harmful for EXPAND_SUGGESTION session command. |
| LOG(ERROR) << "No id found"; |
| return false; |
| } |
| UpdateSelectedCandidateIndex(); |
| return CommitSuggestionInternal(composer, context, consumed_key_size); |
| } |
| |
| void SessionConverter::CommitHeadToFocusedSegments( |
| const composer::Composer &composer, |
| const commands::Context &context, |
| size_t *consumed_key_size) { |
| CommitSegmentsInternal( |
| composer, context, segment_index_ + 1, consumed_key_size); |
| } |
| |
| void SessionConverter::CommitFirstSegment( |
| const composer::Composer &composer, |
| const commands::Context &context, |
| size_t *consumed_key_size) { |
| CommitSegmentsInternal(composer, context, 1, consumed_key_size); |
| } |
| |
| void SessionConverter::CommitSegmentsInternal( |
| const composer::Composer &composer, |
| const commands::Context &context, |
| size_t segments_to_commit, |
| size_t *consumed_key_size) { |
| DCHECK(CheckState(PREDICTION | CONVERSION)); |
| DCHECK(segments_->conversion_segments_size() >= segments_to_commit); |
| ResetResult(); |
| candidate_list_visible_ = false; |
| *consumed_key_size = 0; |
| |
| // If the number of segments is one, just call Commit. |
| if (segments_->conversion_segments_size() == segments_to_commit) { |
| Commit(composer, context); |
| return; |
| } |
| |
| // Store the first conversion segment to the result. |
| if (!UpdateResult(0, segments_to_commit, NULL)) { |
| // If the selected candidate of the first segment has the command |
| // attribute, Cancel is performed instead of Commit. |
| Cancel(); |
| ResetState(); |
| return; |
| } |
| |
| vector<size_t> candidate_ids; |
| for (size_t i = 0; i < segments_to_commit; ++i) { |
| // Get the i-th (0 origin) conversion segment and the selected candidate. |
| Segment *segment = segments_->mutable_conversion_segment(i); |
| if (segment == NULL) { |
| LOG(ERROR) << "There is no segment on position " << i; |
| return; |
| } |
| |
| // Accumulate the size of i-th segment's key. |
| // The caller will remove corresponding characters from the composer. |
| *consumed_key_size += Util::CharsLen(segment->key()); |
| |
| // Collect candidate's id for each segment. |
| candidate_ids.push_back(GetCandidateIndexForConverter(i)); |
| } |
| converter_->CommitSegments(segments_.get(), candidate_ids); |
| |
| // Commit the [0, segments_to_commit - 1] conversion segment. |
| CommitUsageStatsWithSegmentsSize(state_, context, segments_to_commit); |
| |
| // Adjust the segment_index, since the [0, segment_to_commit - 1] segments |
| // disappeared. |
| // Note that segment_index_ is unsigned. |
| segment_index_ = segment_index_ > segments_to_commit |
| ? segment_index_ - segments_to_commit : 0; |
| UpdateCandidateList(); |
| } |
| |
| void SessionConverter::CommitPreedit(const composer::Composer &composer, |
| const commands::Context &context) { |
| string key, preedit, normalized_preedit; |
| composer.GetQueryForConversion(&key); |
| composer.GetStringForSubmission(&preedit); |
| TextNormalizer::NormalizePreeditText(preedit, &normalized_preedit); |
| SessionOutput::FillPreeditResult(preedit, result_.get()); |
| |
| ConverterUtil::InitSegmentsFromString(key, normalized_preedit, |
| segments_.get()); |
| |
| CommitUsageStats(SessionConverterInterface::COMPOSITION, context); |
| ConversionRequest conversion_request(&composer, request_); |
| converter_->FinishConversion(conversion_request, segments_.get()); |
| ResetState(); |
| } |
| |
| void SessionConverter::CommitHead( |
| size_t count, const composer::Composer &composer, |
| size_t *consumed_key_size) { |
| string preedit; |
| composer.GetStringForSubmission(&preedit); |
| if (count > preedit.length()) { |
| *consumed_key_size = preedit.length(); |
| } else { |
| *consumed_key_size = count; |
| } |
| preedit = Util::SubString(preedit, 0, *consumed_key_size); |
| string composition; |
| TextNormalizer::NormalizePreeditText(preedit, &composition); |
| SessionOutput::FillPreeditResult(composition, result_.get()); |
| } |
| |
| void SessionConverter::Revert() { |
| converter_->RevertConversion(segments_.get()); |
| } |
| |
| void SessionConverter::SegmentFocusInternal(size_t index) { |
| DCHECK(CheckState(PREDICTION | CONVERSION)); |
| candidate_list_visible_ = false; |
| if (CheckState(PREDICTION)) { |
| return; // Do nothing. |
| } |
| ResetResult(); |
| |
| if (segment_index_ == index) { |
| return; |
| } |
| |
| SegmentFix(); |
| segment_index_ = index; |
| UpdateCandidateList(); |
| } |
| |
| void SessionConverter::SegmentFocusRight() { |
| if (segment_index_ + 1 >= segments_->conversion_segments_size()) { |
| // If |segment_index_| is at the tail of the segments, |
| // focus on the head. |
| SegmentFocusLeftEdge(); |
| } else { |
| SegmentFocusInternal(segment_index_ + 1); |
| } |
| } |
| |
| void SessionConverter::SegmentFocusLast() { |
| const size_t r_edge = segments_->conversion_segments_size() - 1; |
| SegmentFocusInternal(r_edge); |
| } |
| |
| void SessionConverter::SegmentFocusLeft() { |
| if (segment_index_ <= 0) { |
| // If |segment_index_| is at the head of the segments, |
| // focus on the tail. |
| SegmentFocusLast(); |
| } else { |
| SegmentFocusInternal(segment_index_ - 1); |
| } |
| } |
| |
| void SessionConverter::SegmentFocusLeftEdge() { |
| SegmentFocusInternal(0); |
| } |
| |
| void SessionConverter::ResizeSegmentWidth(const composer::Composer &composer, |
| int delta) { |
| DCHECK(CheckState(PREDICTION | CONVERSION)); |
| candidate_list_visible_ = false; |
| if (CheckState(PREDICTION)) { |
| return; // Do nothing. |
| } |
| ResetResult(); |
| |
| const ConversionRequest conversion_request(&composer, request_); |
| if (!converter_->ResizeSegment(segments_.get(), |
| conversion_request, |
| segment_index_, delta)) { |
| return; |
| } |
| |
| UpdateCandidateList(); |
| // Clears selected index of a focused segment and trailing segments. |
| // TODO(hsumita): Keep the indices if the segment type is FIXED_VALUE. |
| selected_candidate_indices_.resize(segments_->conversion_segments_size()); |
| fill(selected_candidate_indices_.begin() + segment_index_ + 1, |
| selected_candidate_indices_.end(), 0); |
| UpdateSelectedCandidateIndex(); |
| } |
| |
| void SessionConverter::SegmentWidthExpand(const composer::Composer &composer) { |
| ResizeSegmentWidth(composer, 1); |
| } |
| |
| void SessionConverter::SegmentWidthShrink(const composer::Composer &composer) { |
| ResizeSegmentWidth(composer, -1); |
| } |
| |
| const Segment::Candidate * |
| SessionConverter::GetSelectedCandidateOfFocusedSegment() const { |
| if (!candidate_list_->focused()) { |
| return NULL; |
| } |
| const Candidate &cand = candidate_list_->focused_candidate(); |
| const Segment &seg = segments_->conversion_segment(segment_index_); |
| return &seg.candidate(cand.id()); |
| } |
| |
| void SessionConverter::CandidateNext(const composer::Composer &composer) { |
| DCHECK(CheckState(PREDICTION | CONVERSION)); |
| ResetResult(); |
| |
| MaybeExpandPrediction(composer); |
| candidate_list_->MoveNext(); |
| candidate_list_visible_ = true; |
| UpdateSelectedCandidateIndex(); |
| SegmentFocus(); |
| } |
| |
| void SessionConverter::CandidateNextPage() { |
| DCHECK(CheckState(PREDICTION | CONVERSION)); |
| ResetResult(); |
| |
| candidate_list_->MoveNextPage(); |
| candidate_list_visible_ = true; |
| UpdateSelectedCandidateIndex(); |
| SegmentFocus(); |
| } |
| |
| void SessionConverter::CandidatePrev() { |
| DCHECK(CheckState(PREDICTION | CONVERSION)); |
| ResetResult(); |
| |
| candidate_list_->MovePrev(); |
| candidate_list_visible_ = true; |
| UpdateSelectedCandidateIndex(); |
| SegmentFocus(); |
| } |
| |
| void SessionConverter::CandidatePrevPage() { |
| DCHECK(CheckState(PREDICTION | CONVERSION)); |
| ResetResult(); |
| |
| candidate_list_->MovePrevPage(); |
| candidate_list_visible_ = true; |
| UpdateSelectedCandidateIndex(); |
| SegmentFocus(); |
| } |
| |
| void SessionConverter::CandidateMoveToId( |
| const int id, const composer::Composer &composer) { |
| DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION)); |
| ResetResult(); |
| |
| if (CheckState(SUGGESTION)) { |
| // This method makes a candidate focused but SUGGESTION state cannot |
| // have focused candidate. |
| // To solve this conflict we call Predict() method to transit to |
| // PREDICTION state, on which existence of focused candidate is acceptable. |
| Predict(composer); |
| } |
| DCHECK(CheckState(PREDICTION | CONVERSION)); |
| |
| candidate_list_->MoveToId(id); |
| candidate_list_visible_ = false; |
| UpdateSelectedCandidateIndex(); |
| SegmentFocus(); |
| } |
| |
| void SessionConverter::CandidateMoveToPageIndex(const size_t index) { |
| DCHECK(CheckState(PREDICTION | CONVERSION)); |
| ResetResult(); |
| |
| candidate_list_->MoveToPageIndex(index); |
| candidate_list_visible_ = false; |
| UpdateSelectedCandidateIndex(); |
| SegmentFocus(); |
| } |
| |
| bool SessionConverter::CandidateMoveToShortcut(const char shortcut) { |
| DCHECK(CheckState(PREDICTION | CONVERSION)); |
| |
| if (!candidate_list_visible_) { |
| VLOG(1) << "Candidate list is not displayed."; |
| return false; |
| } |
| |
| const string &shortcuts = operation_preferences_.candidate_shortcuts; |
| if (shortcuts.empty()) { |
| VLOG(1) << "No shortcuts"; |
| return false; |
| } |
| |
| // Check if the input character is in the shortcut. |
| // TODO(komatsu): Support non ASCII characters such as Unicode and |
| // special keys. |
| const string::size_type index = shortcuts.find(shortcut); |
| if (index == string::npos) { |
| VLOG(1) << "shortcut is not a member of shortcuts."; |
| return false; |
| } |
| |
| if (!candidate_list_->MoveToPageIndex(index)) { |
| VLOG(1) << "shortcut is out of the range."; |
| return false; |
| } |
| UpdateSelectedCandidateIndex(); |
| ResetResult(); |
| SegmentFocus(); |
| return true; |
| } |
| |
| void SessionConverter::SetCandidateListVisible(bool visible) { |
| candidate_list_visible_ = visible; |
| } |
| |
| void SessionConverter::PopOutput( |
| const composer::Composer &composer, commands::Output *output) { |
| FillOutput(composer, output); |
| ResetResult(); |
| } |
| |
| void SessionConverter::FillOutput( |
| const composer::Composer &composer, commands::Output *output) const { |
| if (output == NULL) { |
| LOG(ERROR) << "output is NULL."; |
| return; |
| } |
| if (result_->has_value()) { |
| FillResult(output->mutable_result()); |
| } |
| if (CheckState(COMPOSITION)) { |
| if (!composer.Empty()) { |
| session::SessionOutput::FillPreedit(composer, |
| output->mutable_preedit()); |
| } |
| } |
| if (!IsActive()) { |
| return; |
| } |
| |
| // Composition on Suggestion |
| if (CheckState(SUGGESTION)) { |
| // When the suggestion comes from zero query suggestion, the |
| // composer is empty. In that case, preedit is not rendered. |
| if (!composer.Empty()) { |
| session::SessionOutput::FillPreedit(composer, |
| output->mutable_preedit()); |
| } |
| } else if (CheckState(PREDICTION | CONVERSION)) { |
| // Conversion on Prediction or Conversion |
| FillConversion(output->mutable_preedit()); |
| } |
| // Candidate list |
| if (CheckState(SUGGESTION | PREDICTION | CONVERSION) && |
| candidate_list_visible_) { |
| FillCandidates(output->mutable_candidates()); |
| } |
| #ifndef __native_client__ |
| // All candidate words |
| // In NaCl, we don't use the all candidate word data. |
| if (CheckState(SUGGESTION | PREDICTION | CONVERSION)) { |
| FillAllCandidateWords(output->mutable_all_candidate_words()); |
| } |
| #endif // __native_client__ |
| } |
| |
| // static |
| void SessionConverter::SetConversionPreferences( |
| const ConversionPreferences &preferences, |
| Segments *segments) { |
| segments->set_user_history_enabled(preferences.use_history); |
| segments->set_max_history_segments_size(preferences.max_history_size); |
| } |
| |
| SessionConverter* SessionConverter::Clone() const { |
| SessionConverter *session_converter = |
| new SessionConverter(converter_, request_); |
| |
| // Copy the members in order of their declarations. |
| session_converter->state_ = state_; |
| // TODO(team): copy of |converter_| member. |
| // We cannot copy the member converter_ from SessionConverterInterface because |
| // it doesn't (and shouldn't) define a method like GetConverter(). At the |
| // moment it's ok because the current design guarantees that the converter is |
| // singleton. However, we should refactor such bad design; see also the |
| // comment right above. |
| session_converter->segments_->CopyFrom(*segments_); |
| session_converter->segment_index_ = segment_index_; |
| session_converter->previous_suggestions_.CopyFrom(previous_suggestions_); |
| session_converter->conversion_preferences_ = conversion_preferences(); |
| session_converter->operation_preferences_ = operation_preferences_; |
| session_converter->result_->CopyFrom(*result_); |
| |
| if (session_converter->CheckState(SUGGESTION | PREDICTION | CONVERSION)) { |
| session_converter->UpdateCandidateList(); |
| session_converter->candidate_list_->MoveToId(candidate_list_->focused_id()); |
| session_converter->SetCandidateListVisible(candidate_list_visible_); |
| } |
| |
| session_converter->request_ = request_; |
| session_converter->selected_candidate_indices_ = selected_candidate_indices_; |
| |
| return session_converter; |
| } |
| |
| void SessionConverter::ResetResult() { |
| result_->Clear(); |
| } |
| |
| void SessionConverter::ResetState() { |
| state_ = COMPOSITION; |
| segment_index_ = 0; |
| previous_suggestions_.clear(); |
| candidate_list_visible_ = false; |
| candidate_list_->Clear(); |
| selected_candidate_indices_.clear(); |
| } |
| |
| void SessionConverter::SegmentFocus() { |
| DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION)); |
| converter_->FocusSegmentValue(segments_.get(), |
| segment_index_, |
| GetCandidateIndexForConverter(segment_index_)); |
| } |
| |
| void SessionConverter::SegmentFix() { |
| DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION)); |
| converter_->CommitSegmentValue(segments_.get(), |
| segment_index_, |
| GetCandidateIndexForConverter(segment_index_)); |
| } |
| |
| void SessionConverter::GetPreedit(const size_t index, |
| const size_t size, |
| string *preedit) const { |
| DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION)); |
| DCHECK(index + size <= segments_->conversion_segments_size()); |
| DCHECK(preedit); |
| |
| preedit->clear(); |
| for (size_t i = index; i < size; ++i) { |
| if (CheckState(CONVERSION)) { |
| // In conversion mode, all the key of candidates is same. |
| preedit->append(segments_->conversion_segment(i).key()); |
| } else { |
| DCHECK(CheckState(SUGGESTION | PREDICTION)); |
| // In suggestion or prediction modes, each key may have |
| // different keys, so content_key is used although it is |
| // possibly dropped the conjugational word (ex. the content_key |
| // of "はしる" is "はし"). |
| preedit->append(GetSelectedCandidate(i).content_key); |
| } |
| } |
| } |
| |
| void SessionConverter::GetConversion(const size_t index, |
| const size_t size, |
| string *conversion) const { |
| DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION)); |
| DCHECK(index + size <= segments_->conversion_segments_size()); |
| DCHECK(conversion); |
| |
| conversion->clear(); |
| for (size_t i = index; i < size; ++i) { |
| conversion->append(GetSelectedCandidateValue(i)); |
| } |
| } |
| |
| size_t SessionConverter::GetConsumedPreeditSize(const size_t index, |
| const size_t size) const { |
| DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION)); |
| DCHECK(index + size <= segments_->conversion_segments_size()); |
| |
| if (CheckState(SUGGESTION | PREDICTION)) { |
| DCHECK_EQ(1, size); |
| const Segment &segment = segments_->conversion_segment(0); |
| const int id = GetCandidateIndexForConverter(0); |
| const Segment::Candidate &candidate = segment.candidate(id); |
| return (candidate.attributes & Segment::Candidate::PARTIALLY_KEY_CONSUMED) |
| ? candidate.consumed_key_size : kConsumedAllCharacters; |
| } |
| |
| DCHECK(CheckState(CONVERSION)); |
| size_t result = 0; |
| for (size_t i = index; i < size; ++i) { |
| const int id = GetCandidateIndexForConverter(i); |
| const Segment::Candidate &candidate = |
| segments_->conversion_segment(i).candidate(id); |
| DCHECK(!(candidate.attributes & |
| Segment::Candidate::PARTIALLY_KEY_CONSUMED)); |
| result += Util::CharsLen(segments_->conversion_segment(i).key()); |
| } |
| return result; |
| } |
| |
| bool SessionConverter::MaybePerformCommandCandidate( |
| const size_t index, |
| const size_t size) const { |
| // If a candidate has the command attribute, Cancel is performed |
| // instead of Commit after executing the specified action. |
| for (size_t i = index; i < size; ++i) { |
| const int id = GetCandidateIndexForConverter(i); |
| const Segment::Candidate &candidate = |
| segments_->conversion_segment(i).candidate(id); |
| if (candidate.attributes & Segment::Candidate::COMMAND_CANDIDATE) { |
| switch (candidate.command) { |
| case Segment::Candidate::DEFAULT_COMMAND: |
| // Do nothing |
| break; |
| case Segment::Candidate::ENABLE_INCOGNITO_MODE: |
| SetIncognitoMode(true); |
| break; |
| case Segment::Candidate::DISABLE_INCOGNITO_MODE: |
| SetIncognitoMode(false); |
| break; |
| case Segment::Candidate::ENABLE_PRESENTATION_MODE: |
| SetPresentationMode(true); |
| break; |
| case Segment::Candidate::DISABLE_PRESENTATION_MODE: |
| SetPresentationMode(false); |
| break; |
| default: |
| LOG(WARNING) << "Unknown command: " << candidate.command; |
| break; |
| } |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| bool SessionConverter::UpdateResult(size_t index, size_t size, |
| size_t *consumed_key_size) { |
| DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION)); |
| |
| // If command candidate is performed, result is not updated and |
| // returns false. |
| if (MaybePerformCommandCandidate(index, size)) { |
| return false; |
| } |
| |
| string preedit, conversion; |
| GetPreedit(index, size, &preedit); |
| GetConversion(index, size, &conversion); |
| if (consumed_key_size) { |
| *consumed_key_size = GetConsumedPreeditSize(index, size); |
| } |
| SessionOutput::FillConversionResult(preedit, conversion, result_.get()); |
| return true; |
| } |
| |
| namespace { |
| // Convert transliteration::TransliterationType to id used in the |
| // converter. The id number are negative values, and 0 of |
| // transliteration::TransliterationType is bound for -1 of the id. |
| int GetT13nId(const transliteration::TransliterationType type) { |
| return -(type + 1); |
| } |
| } // namespace |
| |
| void SessionConverter::AppendCandidateList() { |
| DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION)); |
| |
| // Meta candidates are added iff |candidate_list_| is empty. |
| // This is because if |candidate_list_| is not empty we cannot decide |
| // where to add meta candidates, especially use_cascading_window flag |
| // is true (If there are two or more sub candidate lists, and existent |
| // meta candidates are not located in the same list (distributed over |
| // some lists), the most appropriate location to be added new meta candidates |
| // cannot be decided). |
| const bool add_meta_candidates = (candidate_list_->size() == 0); |
| |
| const Segment &segment = segments_->conversion_segment(segment_index_); |
| for (size_t i = candidate_list_->next_available_id(); |
| i < segment.candidates_size(); |
| ++i) { |
| candidate_list_->AddCandidate(i, segment.candidate(i).value); |
| // if candidate has spelling correction attribute, |
| // always display the candidate to let user know the |
| // miss spelled candidate. |
| if (i < 10 && |
| (segment.candidate(i).attributes & |
| Segment::Candidate::SPELLING_CORRECTION)) { |
| candidate_list_visible_ = true; |
| } |
| } |
| |
| const bool focused = ( |
| segments_->request_type() != Segments::SUGGESTION && |
| segments_->request_type() != Segments::PARTIAL_SUGGESTION && |
| segments_->request_type() != Segments::PARTIAL_PREDICTION); |
| candidate_list_->set_focused(focused); |
| |
| if (segment.meta_candidates_size() == 0) { |
| // For suggestion mode, it is natural that T13N is not initialized. |
| if (CheckState(SUGGESTION)) { |
| return; |
| } |
| // For other modes, records |segment| just in case. |
| VLOG(1) << "T13N is not initialized: " << segment.key(); |
| return; |
| } |
| |
| if (!add_meta_candidates) { |
| return; |
| } |
| |
| // Set transliteration candidates |
| CandidateList *transliterations; |
| if (operation_preferences_.use_cascading_window) { |
| const bool kNoRotate = false; |
| transliterations = candidate_list_->AllocateSubCandidateList(kNoRotate); |
| transliterations->set_focused(true); |
| |
| const char kT13nLabel[] = |
| // "そのほかの文字種"; |
| "\xe3\x81\x9d\xe3\x81\xae\xe3\x81\xbb\xe3\x81\x8b\xe3\x81\xae" |
| "\xe6\x96\x87\xe5\xad\x97\xe7\xa8\xae"; |
| transliterations->set_name(kT13nLabel); |
| } else { |
| transliterations = candidate_list_.get(); |
| } |
| |
| // Add transliterations. |
| for (size_t i = 0; i < transliteration::NUM_T13N_TYPES; ++i) { |
| const transliteration::TransliterationType type = |
| transliteration::TransliterationTypeArray[i]; |
| transliterations->AddCandidateWithAttributes( |
| GetT13nId(type), |
| segment.meta_candidate(i).value, |
| GetT13nAttributes(type)); |
| } |
| } |
| |
| void SessionConverter::UpdateCandidateList() { |
| DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION)); |
| candidate_list_->Clear(); |
| AppendCandidateList(); |
| } |
| |
| int SessionConverter::GetCandidateIndexForConverter( |
| const size_t segment_index) const { |
| DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION)); |
| // If segment_index does not point to the focused segment, the value |
| // should be always zero. |
| if (segment_index != segment_index_) { |
| return 0; |
| } |
| return candidate_list_->focused_id(); |
| } |
| |
| string SessionConverter::GetSelectedCandidateValue( |
| const size_t segment_index) const { |
| DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION)); |
| const int id = GetCandidateIndexForConverter(segment_index); |
| const Segment::Candidate &candidate = |
| segments_->conversion_segment(segment_index).candidate(id); |
| if (candidate.attributes & Segment::Candidate::COMMAND_CANDIDATE) { |
| // Return an empty string, however this path should not be reached. |
| return ""; |
| } |
| return candidate.value; |
| } |
| |
| const Segment::Candidate &SessionConverter::GetSelectedCandidate( |
| const size_t segment_index) const { |
| DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION)); |
| const int id = GetCandidateIndexForConverter(segment_index); |
| return segments_->conversion_segment(segment_index).candidate(id); |
| } |
| |
| void SessionConverter::FillConversion(commands::Preedit *preedit) const { |
| DCHECK(CheckState(PREDICTION | CONVERSION)); |
| SessionOutput::FillConversion(*segments_, |
| segment_index_, |
| candidate_list_->focused_id(), |
| preedit); |
| } |
| |
| void SessionConverter::FillResult(commands::Result *result) const { |
| result->CopyFrom(*result_); |
| } |
| |
| void SessionConverter::FillCandidates(commands::Candidates *candidates) const { |
| DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION)); |
| if (!candidate_list_visible_) { |
| return; |
| } |
| |
| // The position to display the candidate window. |
| size_t position = 0; |
| string conversion; |
| for (size_t i = 0; i < segment_index_; ++i) { |
| position += Util::CharsLen(GetSelectedCandidate(i).value); |
| } |
| |
| // Temporarily added to see if this condition is really satisfied in the |
| // real world or not. |
| #ifdef CHANNEL_DEV |
| CHECK_LT(0, segments_->conversion_segments_size()); |
| #endif // CHANNEL_DEV |
| const Segment &segment = segments_->conversion_segment(segment_index_); |
| SessionOutput::FillCandidates( |
| segment, *candidate_list_, position, candidates); |
| |
| // Shortcut keys |
| if (CheckState(PREDICTION | CONVERSION)) { |
| SessionOutput::FillShortcuts(operation_preferences_.candidate_shortcuts, |
| candidates); |
| } |
| |
| // Store category |
| switch (segments_->request_type()) { |
| case Segments::CONVERSION: |
| candidates->set_category(commands::CONVERSION); |
| break; |
| case Segments::PREDICTION: |
| candidates->set_category(commands::PREDICTION); |
| break; |
| case Segments::SUGGESTION: |
| candidates->set_category(commands::SUGGESTION); |
| break; |
| case Segments::PARTIAL_PREDICTION: |
| // Not PREDICTION because we do not want to get focused candidate. |
| candidates->set_category(commands::SUGGESTION); |
| break; |
| case Segments::PARTIAL_SUGGESTION: |
| candidates->set_category(commands::SUGGESTION); |
| break; |
| default: |
| LOG(WARNING) << "Unknown request type: " << segments_->request_type(); |
| candidates->set_category(commands::CONVERSION); |
| break; |
| } |
| |
| if (candidates->has_usages()) { |
| candidates->mutable_usages()->set_category(commands::USAGE); |
| } |
| if (candidates->has_subcandidates()) { |
| // TODO(komatsu): Subcandidate is not always for transliterations. |
| // The category of the subcandidates should be checked. |
| candidates->mutable_subcandidates()->set_category( |
| commands::TRANSLITERATION); |
| } |
| |
| // Store display type |
| candidates->set_display_type(commands::MAIN); |
| if (candidates->has_usages()) { |
| candidates->mutable_usages()->set_display_type(commands::CASCADE); |
| } |
| if (candidates->has_subcandidates()) { |
| // TODO(komatsu): Subcandidate is not always for transliterations. |
| // The category of the subcandidates should be checked. |
| candidates->mutable_subcandidates()->set_display_type(commands::CASCADE); |
| } |
| |
| // Store footer. |
| SessionOutput::FillFooter(candidates->category(), candidates); |
| } |
| |
| |
| void SessionConverter::FillAllCandidateWords( |
| commands::CandidateList *candidates) const { |
| DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION)); |
| commands::Category category; |
| switch (segments_->request_type()) { |
| case Segments::CONVERSION: |
| category = commands::CONVERSION; |
| break; |
| case Segments::PREDICTION: |
| category = commands::PREDICTION; |
| break; |
| case Segments::SUGGESTION: |
| category = commands::SUGGESTION; |
| break; |
| case Segments::PARTIAL_PREDICTION: |
| // Not PREDICTION because we do not want to get focused candidate. |
| category = commands::SUGGESTION; |
| break; |
| case Segments::PARTIAL_SUGGESTION: |
| category = commands::SUGGESTION; |
| break; |
| default: |
| LOG(WARNING) << "Unknown request type: " << segments_->request_type(); |
| category = commands::CONVERSION; |
| break; |
| } |
| |
| const Segment &segment = segments_->conversion_segment(segment_index_); |
| SessionOutput::FillAllCandidateWords( |
| segment, *candidate_list_, category, candidates); |
| } |
| |
| void SessionConverter::SetRequest(const commands::Request *request) { |
| request_ = request; |
| } |
| |
| void SessionConverter::OnStartComposition(const commands::Context &context) { |
| bool revision_changed = false; |
| if (context.has_revision()) { |
| revision_changed = (context.revision() != client_revision_); |
| client_revision_ = context.revision(); |
| } |
| if (!context.has_preceding_text()) { |
| // In this case, reset history segments when the revision is mismatched. |
| if (revision_changed) { |
| converter_->ResetConversion(segments_.get()); |
| } |
| return; |
| } |
| |
| const string &preceding_text = context.preceding_text(); |
| // If preceding text is empty, it is OK to reset the history segments by |
| // calling ResetConversion. |
| if (preceding_text.empty()) { |
| converter_->ResetConversion(segments_.get()); |
| return; |
| } |
| |
| // Hereafter, we keep the existing history segments as long as it is |
| // consistent with the preceding text even when revision_changed is true. |
| string history_text; |
| for (size_t i = 0; i < segments_->segments_size(); ++i) { |
| const Segment &segment = segments_->segment(i); |
| if (segment.segment_type() != Segment::HISTORY) { |
| break; |
| } |
| if (segment.candidates_size() == 0) { |
| break; |
| } |
| history_text.append(segment.candidate(0).value); |
| } |
| |
| if (!history_text.empty()) { |
| // Compare |preceding_text| with |history_text| to check if the history |
| // segments are still valid or not. |
| DCHECK(!preceding_text.empty()); |
| DCHECK(!history_text.empty()); |
| if (preceding_text.size() > history_text.size()) { |
| if (Util::EndsWith(preceding_text, history_text)) { |
| // History segments seem to be consistent with preceding text. |
| return; |
| } |
| } else { |
| if (Util::EndsWith(history_text, preceding_text)) { |
| // History segments seem to be consistent with preceding text. |
| return; |
| } |
| } |
| } |
| |
| // Here we reconstruct history segments from |preceding_text| regardless |
| // of revision mismatch. If it fails the history segments is cleared anyway. |
| converter_->ReconstructHistory(segments_.get(), preceding_text); |
| } |
| |
| void SessionConverter::UpdateSelectedCandidateIndex() { |
| int index; |
| const Candidate &focused_candidate = candidate_list_->focused_candidate(); |
| if (focused_candidate.IsSubcandidateList()) { |
| const int t13n_index = |
| focused_candidate.subcandidate_list().focused_index(); |
| index = -1 - t13n_index; |
| } else { |
| // TODO(hsumita): Use id instead of focused index. |
| index = candidate_list_->focused_index(); |
| } |
| selected_candidate_indices_[segment_index_] = index; |
| } |
| |
| void SessionConverter::InitializeSelectedCandidateIndices() { |
| selected_candidate_indices_.clear(); |
| selected_candidate_indices_.resize(segments_->conversion_segments_size()); |
| } |
| |
| void SessionConverter::UpdateCandidateStats(const string &base_name, |
| int32 index) { |
| string prefix; |
| if (index < 0) { |
| prefix = "TransliterationCandidates"; |
| index = -1 - index; |
| } else { |
| prefix = base_name + "Candidates"; |
| } |
| |
| if (index <= 9) { |
| const string stats_name = prefix + NumberUtil::SimpleItoa(index); |
| UsageStats::IncrementCount(stats_name); |
| } else { |
| const string stats_name = prefix + "GE10"; |
| UsageStats::IncrementCount(stats_name); |
| } |
| } |
| |
| void SessionConverter::CommitUsageStats( |
| SessionConverterInterface::State commit_state, |
| const commands::Context &context) { |
| size_t commit_segment_size = 0; |
| switch (commit_state) { |
| case COMPOSITION: |
| commit_segment_size = 0; |
| break; |
| case SUGGESTION: |
| case PREDICTION: |
| commit_segment_size = 1; |
| break; |
| case CONVERSION: |
| commit_segment_size = segments_->conversion_segments_size(); |
| break; |
| default: |
| LOG(DFATAL) << "Unexpected state: " << commit_state; |
| } |
| CommitUsageStatsWithSegmentsSize(commit_state, context, commit_segment_size); |
| } |
| |
| void SessionConverter::CommitUsageStatsWithSegmentsSize( |
| SessionConverterInterface::State commit_state, |
| const commands::Context &context, |
| size_t commit_segments_size) { |
| CHECK_LE(commit_segments_size, selected_candidate_indices_.size()); |
| |
| string stats_str; |
| switch (commit_state) { |
| case COMPOSITION: |
| stats_str = "Composition"; |
| break; |
| case SUGGESTION: |
| case PREDICTION: |
| // Suggestion related usage stats are collected as Prediction. |
| stats_str = "Prediction"; |
| UpdateCandidateStats(stats_str, selected_candidate_indices_[0]); |
| break; |
| case CONVERSION: |
| stats_str = "Conversion"; |
| for (size_t i = 0; i < commit_segments_size; ++i) { |
| UpdateCandidateStats(stats_str, |
| selected_candidate_indices_[i]); |
| } |
| break; |
| default: |
| LOG(DFATAL) << "Unexpected state: " << commit_state; |
| stats_str = "Unknown"; |
| } |
| |
| UsageStats::IncrementCount("Commit"); |
| UsageStats::IncrementCount("CommitFrom" + stats_str); |
| |
| if (stats_str != "Unknown") { |
| if (SessionUsageStatsUtil::HasExperimentalFeature(context, |
| "chrome_omnibox")) { |
| UsageStats::IncrementCount("CommitFrom" + stats_str + "InChromeOmnibox"); |
| } |
| if (SessionUsageStatsUtil::HasExperimentalFeature(context, |
| "google_search_box")) { |
| UsageStats::IncrementCount( |
| "CommitFrom" + stats_str + "InGoogleSearchBox"); |
| } |
| } |
| |
| const vector<int>::iterator it = selected_candidate_indices_.begin(); |
| selected_candidate_indices_.erase(it, it + commit_segments_size); |
| } |
| |
| } // namespace session |
| } // namespace mozc |