1 // Copyright 2010-2018, Google Inc.
2 // All rights reserved.
3 //
4 // Redistribution and use in source and binary forms, with or without
5 // modification, are permitted provided that the following conditions are
6 // met:
7 //
8 // * Redistributions of source code must retain the above copyright
9 // notice, this list of conditions and the following disclaimer.
10 // * Redistributions in binary form must reproduce the above
11 // copyright notice, this list of conditions and the following disclaimer
12 // in the documentation and/or other materials provided with the
13 // distribution.
14 // * Neither the name of Google Inc. nor the names of its
15 // contributors may be used to endorse or promote products derived from
16 // this software without specific prior written permission.
17 //
18 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30 // A class handling the converter on the session layer.
31
32 #include "session/session_converter.h"
33
34 #include <algorithm>
35 #include <limits>
36 #include <string>
37
38 #include "base/flags.h"
39 #include "base/logging.h"
40 #include "base/port.h"
41 #include "base/text_normalizer.h"
42 #include "base/util.h"
43 #include "composer/composer.h"
44 #include "config/config_handler.h"
45 #include "converter/converter_interface.h"
46 #include "converter/converter_util.h"
47 #include "converter/segments.h"
48 #include "protocol/commands.pb.h"
49 #include "protocol/config.pb.h"
50 #include "request/conversion_request.h"
51 #include "session/internal/candidate_list.h"
52 #include "session/internal/session_output.h"
53 #include "session/session_usage_stats_util.h"
54 #include "transliteration/transliteration.h"
55 #include "usage_stats/usage_stats.h"
56
57 using mozc::usage_stats::UsageStats;
58
59 #ifdef OS_ANDROID
60 const bool kDefaultUseActualConverterForRealtimeConversion = false;
61 #else
62 const bool kDefaultUseActualConverterForRealtimeConversion = true;
63 #endif // OS_ANDROID
64
65 DEFINE_bool(use_actual_converter_for_realtime_conversion,
66 kDefaultUseActualConverterForRealtimeConversion,
67 "If true, use the actual (non-immutable) converter for real "
68 "time conversion.");
69
70 namespace mozc {
71 namespace session {
72
73 namespace {
74
75 using mozc::commands::Request;
76 using mozc::config::Config;
77 using mozc::config::ConfigHandler;
78
79 const size_t kDefaultMaxHistorySize = 3;
80
GetCandidateShortcuts(config::Config::SelectionShortcut selection_shortcut)81 const char *GetCandidateShortcuts(
82 config::Config::SelectionShortcut selection_shortcut) {
83 // Keyboard shortcut for candidates.
84 const char *kShortcut123456789 = "123456789";
85 const char *kShortcutASDFGHJKL = "asdfghjkl";
86 const char *kNoShortcut = "";
87
88 const char *shortcut = kNoShortcut;
89 switch (selection_shortcut) {
90 case config::Config::SHORTCUT_123456789:
91 shortcut = kShortcut123456789;
92 break;
93 case config::Config::SHORTCUT_ASDFGHJKL:
94 shortcut = kShortcutASDFGHJKL;
95 break;
96 case config::Config::NO_SHORTCUT:
97 break;
98 default:
99 LOG(WARNING) << "Unknown shortcuts type: " << selection_shortcut;
100 break;
101 }
102 return shortcut;
103 }
104
105 } // namespace
106
107 const size_t SessionConverter::kConsumedAllCharacters =
108 std::numeric_limits<size_t>::max();
109
SessionConverter(const ConverterInterface * converter,const Request * request,const Config * config)110 SessionConverter::SessionConverter(const ConverterInterface *converter,
111 const Request *request,
112 const Config *config)
113 : SessionConverterInterface(),
114 state_(COMPOSITION),
115 converter_(converter),
116 segments_(new Segments),
117 segment_index_(0),
118 result_(new commands::Result),
119 candidate_list_(new CandidateList(true)),
120 candidate_list_visible_(false),
121 request_(request),
122 client_revision_(0) {
123 conversion_preferences_.use_history = true;
124 conversion_preferences_.max_history_size = kDefaultMaxHistorySize;
125 conversion_preferences_.request_suggestion = true;
126 candidate_list_->set_page_size(request->candidate_page_size());
127 SetConfig(config);
128 }
129
~SessionConverter()130 SessionConverter::~SessionConverter() {}
131
CheckState(SessionConverterInterface::States states) const132 bool SessionConverter::CheckState(
133 SessionConverterInterface::States states) const {
134 return ((state_ & states) != NO_STATE);
135 }
136
IsActive() const137 bool SessionConverter::IsActive() const {
138 return CheckState(SUGGESTION | PREDICTION | CONVERSION);
139 }
140
conversion_preferences() const141 const ConversionPreferences &SessionConverter::conversion_preferences() const {
142 return conversion_preferences_;
143 }
144
Convert(const composer::Composer & composer)145 bool SessionConverter::Convert(const composer::Composer &composer) {
146 return ConvertWithPreferences(composer, conversion_preferences_);
147 }
148
ConvertWithPreferences(const composer::Composer & composer,const ConversionPreferences & preferences)149 bool SessionConverter::ConvertWithPreferences(
150 const composer::Composer &composer,
151 const ConversionPreferences &preferences) {
152 DCHECK(CheckState(COMPOSITION | SUGGESTION | CONVERSION));
153
154 segments_->set_request_type(Segments::CONVERSION);
155 SetConversionPreferences(preferences, segments_.get());
156
157 const ConversionRequest conversion_request(&composer, request_, config_);
158 if (!converter_->StartConversionForRequest(conversion_request,
159 segments_.get())) {
160 LOG(WARNING) << "StartConversionForRequest() failed";
161 ResetState();
162 return false;
163 }
164
165 segment_index_ = 0;
166 state_ = CONVERSION;
167 candidate_list_visible_ = false;
168 UpdateCandidateList();
169 InitializeSelectedCandidateIndices();
170 return true;
171 }
172
GetReadingText(const string & source_text,string * reading)173 bool SessionConverter::GetReadingText(const string &source_text,
174 string *reading) {
175 DCHECK(reading);
176 reading->clear();
177 Segments reverse_segments;
178 if (!converter_->StartReverseConversion(&reverse_segments, source_text)) {
179 return false;
180 }
181 if (reverse_segments.segments_size() == 0) {
182 LOG(WARNING) << "no segments from reverse conversion";
183 return false;
184 }
185 for (size_t i = 0; i < reverse_segments.segments_size(); ++i) {
186 const mozc::Segment &segment = reverse_segments.segment(i);
187 if (segment.candidates_size() == 0) {
188 LOG(WARNING) << "got an empty segment from reverse conversion";
189 return false;
190 }
191 reading->append(segment.candidate(0).value);
192 }
193 return true;
194 }
195
196 namespace {
GetT13nAttributes(const transliteration::TransliterationType type)197 Attributes GetT13nAttributes(const transliteration::TransliterationType type) {
198 Attributes attributes = NO_ATTRIBUTES;
199 switch (type) {
200 case transliteration::HIRAGANA: // "ひらがな"
201 attributes = HIRAGANA;
202 break;
203 case transliteration::FULL_KATAKANA: // "カタカナ"
204 attributes = (FULL_WIDTH | KATAKANA);
205 break;
206 case transliteration::HALF_ASCII: // "ascII"
207 attributes = (HALF_WIDTH | ASCII);
208 break;
209 case transliteration::HALF_ASCII_UPPER: // "ASCII"
210 attributes = (HALF_WIDTH | ASCII | UPPER);
211 break;
212 case transliteration::HALF_ASCII_LOWER: // "ascii"
213 attributes = (HALF_WIDTH | ASCII | LOWER);
214 break;
215 case transliteration::HALF_ASCII_CAPITALIZED: // "Ascii"
216 attributes = (HALF_WIDTH | ASCII | CAPITALIZED);
217 break;
218 case transliteration::FULL_ASCII: // "ascII"
219 attributes = (FULL_WIDTH | ASCII);
220 break;
221 case transliteration::FULL_ASCII_UPPER: // "ASCII"
222 attributes = (FULL_WIDTH | ASCII | UPPER);
223 break;
224 case transliteration::FULL_ASCII_LOWER: // "ascii"
225 attributes = (FULL_WIDTH | ASCII | LOWER);
226 break;
227 case transliteration::FULL_ASCII_CAPITALIZED: // "Ascii"
228 attributes = (FULL_WIDTH | ASCII | CAPITALIZED);
229 break;
230 case transliteration::HALF_KATAKANA: // "カタカナ"
231 attributes = (HALF_WIDTH | KATAKANA);
232 break;
233 default:
234 LOG(ERROR) << "Unknown type: " << type;
235 break;
236 }
237 return attributes;
238 }
239 } // namespace
240
ConvertToTransliteration(const composer::Composer & composer,const transliteration::TransliterationType type)241 bool SessionConverter::ConvertToTransliteration(
242 const composer::Composer &composer,
243 const transliteration::TransliterationType type) {
244 DCHECK(CheckState(COMPOSITION | SUGGESTION | PREDICTION | CONVERSION));
245 if (CheckState(PREDICTION)) {
246 // TODO(komatsu): A better way is to transliterate the key of the
247 // focused candidate. However it takes a long time.
248 Cancel();
249 DCHECK(CheckState(COMPOSITION));
250 }
251
252 Attributes query_attr =
253 (GetT13nAttributes(type) &
254 (HALF_WIDTH | FULL_WIDTH | ASCII | HIRAGANA | KATAKANA));
255
256 if (CheckState(COMPOSITION | SUGGESTION)) {
257 if (!Convert(composer)) {
258 LOG(ERROR) << "Conversion failed";
259 return false;
260 }
261
262 // TODO(komatsu): This is a workaround to transliterate the whole
263 // preedit as a single segment. We should modify
264 // converter/converter.cc to enable to accept mozc::Segment::FIXED
265 // from the session layer.
266 if (segments_->conversion_segments_size() != 1) {
267 string composition;
268 GetPreedit(0, segments_->conversion_segments_size(), &composition);
269 ResizeSegmentWidth(composer, Util::CharsLen(composition));
270 }
271
272 DCHECK(CheckState(CONVERSION));
273 candidate_list_->MoveToAttributes(query_attr);
274 } else {
275 DCHECK(CheckState(CONVERSION));
276 const Attributes current_attr =
277 candidate_list_->GetDeepestFocusedCandidate().attributes();
278
279 if ((query_attr & current_attr & ASCII) &&
280 ((((query_attr & HALF_WIDTH) && (current_attr & FULL_WIDTH))) ||
281 (((query_attr & FULL_WIDTH) && (current_attr & HALF_WIDTH))))) {
282 query_attr |= (current_attr & (UPPER | LOWER | CAPITALIZED));
283 }
284
285 candidate_list_->MoveNextAttributes(query_attr);
286 }
287 candidate_list_visible_ = false;
288 // Treat as top conversion candidate on usage stats.
289 selected_candidate_indices_[segment_index_] = 0;
290 SegmentFocus();
291 return true;
292 }
293
ConvertToHalfWidth(const composer::Composer & composer)294 bool SessionConverter::ConvertToHalfWidth(const composer::Composer &composer) {
295 DCHECK(CheckState(COMPOSITION | SUGGESTION | PREDICTION | CONVERSION));
296 if (CheckState(PREDICTION)) {
297 // TODO(komatsu): A better way is to transliterate the key of the
298 // focused candidate. However it takes a long time.
299 Cancel();
300 DCHECK(CheckState(COMPOSITION));
301 }
302
303 string composition;
304 if (CheckState(COMPOSITION | SUGGESTION)) {
305 composer.GetStringForPreedit(&composition);
306 } else {
307 composition = GetSelectedCandidate(segment_index_).value;
308 }
309
310 // TODO(komatsu): make a function to return a logical sum of ScriptType.
311 // If composition_ is "あbc", it should be treated as Katakana.
312 if (Util::ContainsScriptType(composition, Util::KATAKANA) ||
313 Util::ContainsScriptType(composition, Util::HIRAGANA) ||
314 Util::ContainsScriptType(composition, Util::KANJI) ||
315 Util::IsKanaSymbolContained(composition)) {
316 return ConvertToTransliteration(composer, transliteration::HALF_KATAKANA);
317 } else {
318 return ConvertToTransliteration(composer, transliteration::HALF_ASCII);
319 }
320 }
321
SwitchKanaType(const composer::Composer & composer)322 bool SessionConverter::SwitchKanaType(const composer::Composer &composer) {
323 DCHECK(CheckState(COMPOSITION | SUGGESTION | PREDICTION | CONVERSION));
324 if (CheckState(PREDICTION)) {
325 // TODO(komatsu): A better way is to transliterate the key of the
326 // focused candidate. However it takes a long time.
327 Cancel();
328 DCHECK(CheckState(COMPOSITION));
329 }
330
331 Attributes attributes = NO_ATTRIBUTES;
332 if (CheckState(COMPOSITION | SUGGESTION)) {
333 if (!Convert(composer)) {
334 LOG(ERROR) << "Conversion failed";
335 return false;
336 }
337
338 // TODO(komatsu): This is a workaround to transliterate the whole
339 // preedit as a single segment. We should modify
340 // converter/converter.cc to enable to accept mozc::Segment::FIXED
341 // from the session layer.
342 if (segments_->conversion_segments_size() != 1) {
343 string composition;
344 GetPreedit(0, segments_->conversion_segments_size(), &composition);
345 const ConversionRequest conversion_request(&composer, request_, config_);
346 converter_->ResizeSegment(segments_.get(),
347 conversion_request,
348 0, Util::CharsLen(composition));
349 UpdateCandidateList();
350 }
351
352 attributes = (FULL_WIDTH | KATAKANA);
353 } else {
354 const Attributes current_attributes =
355 candidate_list_->GetDeepestFocusedCandidate().attributes();
356 // "漢字" -> "かんじ" -> "カンジ" -> "カンジ" -> "かんじ" -> ...
357 if (current_attributes & HIRAGANA) {
358 attributes = (FULL_WIDTH | KATAKANA);
359 } else if ((current_attributes & KATAKANA) &&
360 (current_attributes & FULL_WIDTH)) {
361 attributes = (HALF_WIDTH | KATAKANA);
362 } else {
363 attributes = HIRAGANA;
364 }
365 }
366
367 DCHECK(CheckState(CONVERSION));
368 candidate_list_->MoveNextAttributes(attributes);
369 candidate_list_visible_ = false;
370 // Treat as top conversion candidate on usage stats.
371 selected_candidate_indices_[segment_index_] = 0;
372 SegmentFocus();
373 return true;
374 }
375
376 namespace {
377
378 // Prepend the candidates to the first conversion segment.
PrependCandidates(const Segment & previous_segment,const string & preedit,Segments * segments)379 void PrependCandidates(const Segment &previous_segment,
380 const string &preedit,
381 Segments *segments) {
382 DCHECK(segments);
383
384 // TODO(taku) want to have a method in converter to make an empty segment
385 if (segments->conversion_segments_size() == 0) {
386 segments->clear_conversion_segments();
387 Segment *segment = segments->add_segment();
388 segment->Clear();
389 segment->set_key(preedit);
390 }
391
392 DCHECK_EQ(1, segments->conversion_segments_size());
393 Segment *segment = segments->mutable_conversion_segment(0);
394 DCHECK(segment);
395
396 const size_t cands_size = previous_segment.candidates_size();
397 for (size_t i = 0; i < cands_size; ++i) {
398 Segment::Candidate *candidate = segment->push_front_candidate();
399 candidate->CopyFrom(previous_segment.candidate(cands_size - i - 1));
400 }
401 *(segment->mutable_meta_candidates()) = previous_segment.meta_candidates();
402 }
403 } // namespace
404
405
Suggest(const composer::Composer & composer)406 bool SessionConverter::Suggest(const composer::Composer &composer) {
407 return SuggestWithPreferences(composer, conversion_preferences_);
408 }
409
SuggestWithPreferences(const composer::Composer & composer,const ConversionPreferences & preferences)410 bool SessionConverter::SuggestWithPreferences(
411 const composer::Composer &composer,
412 const ConversionPreferences &preferences) {
413 DCHECK(CheckState(COMPOSITION | SUGGESTION));
414 candidate_list_visible_ = false;
415
416 // Normalize the current state by resetting the previous state.
417 ResetState();
418
419 // If we are on a password field, suppress suggestion.
420 if (!preferences.request_suggestion ||
421 composer.GetInputFieldType() == commands::Context::PASSWORD) {
422 return false;
423 }
424
425 // Initialize the segments for suggestion.
426 SetConversionPreferences(preferences, segments_.get());
427
428 ConversionRequest conversion_request(&composer, request_, config_);
429 const size_t cursor = composer.GetCursor();
430 if (cursor == composer.GetLength() || cursor == 0 ||
431 !request_->mixed_conversion()) {
432 conversion_request.set_create_partial_candidates(
433 request_->auto_partial_suggestion());
434 conversion_request.set_use_actual_converter_for_realtime_conversion(
435 FLAGS_use_actual_converter_for_realtime_conversion);
436 if (!converter_->StartSuggestionForRequest(conversion_request,
437 segments_.get())) {
438 // TODO(komatsu): Because suggestion is a prefix search, once
439 // StartSuggestion returns false, this GetSuggestion always
440 // returns false. Refactor it.
441 VLOG(1) << "StartSuggestionForRequest() returns no suggestions.";
442 // Clear segments and keep the context
443 converter_->CancelConversion(segments_.get());
444 return false;
445 }
446 } else {
447 // create_partial_candidates is false because auto partial suggestion
448 // should be activated only when the cursor is at the tail or head from
449 // the view point of UX.
450 // use_actual_converter_for_realtime_conversion is also false because of
451 // implementation reason. If the flag is true, all the composition
452 // characters will be used in the below process, which conflicts
453 // with *partial* prediction.
454 if (!converter_->StartPartialSuggestionForRequest(conversion_request,
455 segments_.get())) {
456 VLOG(1) << "StartPartialSuggestionForRequest() returns no suggestions.";
457 // Clear segments and keep the context
458 converter_->CancelConversion(segments_.get());
459 return false;
460 }
461 }
462 DCHECK_EQ(1, segments_->conversion_segments_size());
463
464 // Copy current suggestions so that we can merge
465 // prediction/suggestions later
466 previous_suggestions_.CopyFrom(segments_->conversion_segment(0));
467
468 // TODO(komatsu): the next line can be deleted.
469 segment_index_ = 0;
470 state_ = SUGGESTION;
471 UpdateCandidateList();
472 candidate_list_visible_ = true;
473 InitializeSelectedCandidateIndices();
474 return true;
475 }
476
477
Predict(const composer::Composer & composer)478 bool SessionConverter::Predict(const composer::Composer &composer) {
479 return PredictWithPreferences(composer, conversion_preferences_);
480 }
481
IsEmptySegment(const Segment & segment) const482 bool SessionConverter::IsEmptySegment(const Segment &segment) const {
483 return ((segment.candidates_size() == 0) &&
484 (segment.meta_candidates_size() == 0));
485 }
486
PredictWithPreferences(const composer::Composer & composer,const ConversionPreferences & preferences)487 bool SessionConverter::PredictWithPreferences(
488 const composer::Composer &composer,
489 const ConversionPreferences &preferences) {
490 // TODO(komatsu): DCHECK should be
491 // DCHECK(CheckState(COMPOSITION | SUGGESTION | PREDICTION));
492 DCHECK(CheckState(COMPOSITION | SUGGESTION | CONVERSION | PREDICTION));
493 ResetResult();
494
495 // Initialize the segments for prediction
496 segments_->set_request_type(Segments::PREDICTION);
497 SetConversionPreferences(preferences, segments_.get());
498
499 const bool predict_first =
500 !CheckState(PREDICTION) && IsEmptySegment(previous_suggestions_);
501
502 const bool predict_expand =
503 (CheckState(PREDICTION) &&
504 !IsEmptySegment(previous_suggestions_) &&
505 candidate_list_->size() > 0 &&
506 candidate_list_->focused() &&
507 candidate_list_->focused_index() == candidate_list_->last_index());
508
509 segments_->clear_conversion_segments();
510
511 if (predict_expand || predict_first) {
512 ConversionRequest conversion_request(&composer, request_, config_);
513 conversion_request.set_use_actual_converter_for_realtime_conversion(
514 FLAGS_use_actual_converter_for_realtime_conversion);
515 if (!converter_->StartPredictionForRequest(conversion_request,
516 segments_.get())) {
517 LOG(WARNING) << "StartPredictionForRequest() failed";
518
519 // TODO(komatsu): Perform refactoring after checking the stability test.
520 //
521 // If predict_expand is true, it means we have prevous_suggestions_.
522 // So we can use it as the result of this prediction.
523 if (predict_first) {
524 ResetState();
525 return false;
526 }
527 }
528 }
529
530 // Merge suggestions and prediction
531 string preedit;
532 composer.GetQueryForPrediction(&preedit);
533 PrependCandidates(previous_suggestions_, preedit, segments_.get());
534
535 segment_index_ = 0;
536 state_ = PREDICTION;
537 UpdateCandidateList();
538 candidate_list_visible_ = true;
539 InitializeSelectedCandidateIndices();
540
541 return true;
542 }
543
ExpandSuggestion(const composer::Composer & composer)544 bool SessionConverter::ExpandSuggestion(const composer::Composer &composer) {
545 return ExpandSuggestionWithPreferences(composer, conversion_preferences_);
546 }
547
ExpandSuggestionWithPreferences(const composer::Composer & composer,const ConversionPreferences & preferences)548 bool SessionConverter::ExpandSuggestionWithPreferences(
549 const composer::Composer &composer,
550 const ConversionPreferences &preferences) {
551 DCHECK(CheckState(COMPOSITION | SUGGESTION | PREDICTION));
552 if (CheckState(COMPOSITION)) {
553 // Client can send EXPAND_SUGGESTION command when on composition mode.
554 // In such case we do nothing.
555 VLOG(1) << "ExpandSuggestion does nothing on composition mode.";
556 return false;
557 }
558
559 ResetResult();
560
561 // Expand suggestion.
562 // Current implementation is hacky.
563 // We want prediction candidates,
564 // but want to set candidates' category SUGGESTION.
565 // TODO(matsuzakit or yamaguchi): Refactor following lines,
566 // after implemention of partial conversion.
567
568 // Initialize the segments for prediction.
569 SetConversionPreferences(preferences, segments_.get());
570
571 string preedit;
572 composer.GetQueryForPrediction(&preedit);
573
574 // We do not need "segments_->clear_conversion_segments()".
575 // Without this statement we can add additional candidates into
576 // existing segments.
577
578 ConversionRequest conversion_request(&composer, request_, config_);
579
580 const size_t cursor = composer.GetCursor();
581 if (cursor == composer.GetLength() || cursor == 0 ||
582 !request_->mixed_conversion()) {
583 conversion_request.set_create_partial_candidates(
584 request_->auto_partial_suggestion());
585 conversion_request.set_use_actual_converter_for_realtime_conversion(
586 FLAGS_use_actual_converter_for_realtime_conversion);
587 // This is abuse of StartPrediction().
588 // TODO(matsuzakit or yamaguchi): Add ExpandSuggestion method
589 // to Converter class.
590 if (!converter_->StartPredictionForRequest(conversion_request,
591 segments_.get())) {
592 LOG(WARNING) << "StartPredictionForRequest() failed";
593 }
594 } else {
595 // c.f. SuggestWithPreferences for ConversionRequest flags.
596 if (!converter_->StartPartialPredictionForRequest(conversion_request,
597 segments_.get())) {
598 VLOG(1) << "StartPartialPredictionForRequest() returns no suggestions.";
599 // Clear segments and keep the context
600 converter_->CancelConversion(segments_.get());
601 return false;
602 }
603 }
604 // Overwrite the request type to SUGGESTION.
605 // Without this logic, a candidate gets focused that is unexpected behavior.
606 segments_->set_request_type(Segments::SUGGESTION);
607
608 // Merge suggestions and predictions.
609 PrependCandidates(previous_suggestions_, preedit, segments_.get());
610
611 segment_index_ = 0;
612 // Call AppendCandidateList instead of UpdateCandidateList because
613 // we want to keep existing candidates.
614 // As a result, ExpandSuggestionWithPreferences adds expanded suggestion
615 // candidates at the tail of existing candidates.
616 AppendCandidateList();
617 candidate_list_visible_ = true;
618 return true;
619 }
620
MaybeExpandPrediction(const composer::Composer & composer)621 void SessionConverter::MaybeExpandPrediction(
622 const composer::Composer &composer) {
623 DCHECK(CheckState(PREDICTION | CONVERSION));
624
625 // Expand the current suggestions and fill with Prediction results.
626 if (!CheckState(PREDICTION) ||
627 IsEmptySegment(previous_suggestions_) ||
628 !candidate_list_->focused() ||
629 candidate_list_->focused_index() != candidate_list_->last_index()) {
630 return;
631 }
632
633 DCHECK(CheckState(PREDICTION));
634 ResetResult();
635
636 const size_t previous_index = candidate_list_->focused_index();
637 if (!PredictWithPreferences(composer, conversion_preferences_)) {
638 return;
639 }
640
641 DCHECK_LT(previous_index, candidate_list_->size());
642 candidate_list_->MoveToId(candidate_list_->candidate(previous_index).id());
643 UpdateSelectedCandidateIndex();
644 }
645
Cancel()646 void SessionConverter::Cancel() {
647 DCHECK(CheckState(PREDICTION | CONVERSION));
648 ResetResult();
649
650 // Clear segments and keep the context
651 converter_->CancelConversion(segments_.get());
652 ResetState();
653 }
654
Reset()655 void SessionConverter::Reset() {
656 DCHECK(CheckState(COMPOSITION | SUGGESTION | PREDICTION | CONVERSION));
657
658 // Even if composition mode, call ResetConversion
659 // in order to clear history segments.
660 converter_->ResetConversion(segments_.get());
661
662 if (CheckState(COMPOSITION)) {
663 return;
664 }
665
666 ResetResult();
667 // Reset segments (and its internal context)
668 ResetState();
669 }
670
Commit(const composer::Composer & composer,const commands::Context & context)671 void SessionConverter::Commit(const composer::Composer &composer,
672 const commands::Context &context) {
673 DCHECK(CheckState(PREDICTION | CONVERSION));
674 ResetResult();
675
676 if (!UpdateResult(0, segments_->conversion_segments_size(), NULL)) {
677 Cancel();
678 ResetState();
679 return;
680 }
681
682 for (size_t i = 0; i < segments_->conversion_segments_size(); ++i) {
683 converter_->CommitSegmentValue(segments_.get(),
684 i,
685 GetCandidateIndexForConverter(i));
686 }
687 CommitUsageStats(state_, context);
688 ConversionRequest conversion_request(&composer, request_, config_);
689 converter_->FinishConversion(conversion_request, segments_.get());
690 ResetState();
691 }
692
CommitSuggestionInternal(const composer::Composer & composer,const commands::Context & context,size_t * consumed_key_size)693 bool SessionConverter::CommitSuggestionInternal(
694 const composer::Composer &composer,
695 const commands::Context &context,
696 size_t *consumed_key_size) {
697 DCHECK(consumed_key_size);
698 DCHECK(CheckState(SUGGESTION));
699 ResetResult();
700 string preedit;
701 composer.GetStringForPreedit(&preedit);
702
703 if (!UpdateResult(0, segments_->conversion_segments_size(),
704 consumed_key_size)) {
705 // Do not need to call Cancel like Commit because the current
706 // state is SUGGESTION.
707 ResetState();
708 return false;
709 }
710
711 const size_t preedit_length = Util::CharsLen(preedit);
712
713 // TODO(horo): When we will support hardware keyboard and introduce
714 // shift+enter keymap in Android, this if condition may be insufficient.
715 if (request_->zero_query_suggestion() &&
716 *consumed_key_size < composer.GetLength()) {
717 // A candidate was chosen from partial suggestion.
718 converter_->CommitPartialSuggestionSegmentValue(
719 segments_.get(),
720 0,
721 GetCandidateIndexForConverter(0),
722 Util::SubString(preedit, 0, *consumed_key_size),
723 Util::SubString(preedit,
724 *consumed_key_size,
725 preedit_length - *consumed_key_size));
726 CommitUsageStats(SessionConverterInterface::SUGGESTION, context);
727 InitializeSelectedCandidateIndices();
728 // One or more segments must exist because new segment is inserted
729 // just after the commited segment.
730 DCHECK_GT(segments_->conversion_segments_size(), 0);
731 } else {
732 // Not partial suggestion so let's reset the state.
733 converter_->CommitSegmentValue(segments_.get(),
734 0,
735 GetCandidateIndexForConverter(0));
736 CommitUsageStats(SessionConverterInterface::SUGGESTION, context);
737 ConversionRequest conversion_request(&composer, request_, config_);
738 converter_->FinishConversion(conversion_request, segments_.get());
739 DCHECK_EQ(0, segments_->conversion_segments_size());
740 ResetState();
741 }
742 return true;
743 }
744
CommitSuggestionByIndex(const size_t index,const composer::Composer & composer,const commands::Context & context,size_t * consumed_key_size)745 bool SessionConverter::CommitSuggestionByIndex(
746 const size_t index,
747 const composer::Composer &composer,
748 const commands::Context &context,
749 size_t *consumed_key_size) {
750 DCHECK(CheckState(SUGGESTION));
751 if (index >= candidate_list_->size()) {
752 LOG(ERROR) << "index is out of the range: " << index;
753 return false;
754 }
755 candidate_list_->MoveToPageIndex(index);
756 UpdateSelectedCandidateIndex();
757 return CommitSuggestionInternal(composer, context, consumed_key_size);
758 }
759
CommitSuggestionById(const int id,const composer::Composer & composer,const commands::Context & context,size_t * consumed_key_size)760 bool SessionConverter::CommitSuggestionById(
761 const int id,
762 const composer::Composer &composer,
763 const commands::Context &context,
764 size_t *consumed_key_size) {
765 DCHECK(CheckState(SUGGESTION));
766 if (!candidate_list_->MoveToId(id)) {
767 // Don't use CandidateMoveToId() method, which overwrites candidates.
768 // This is harmful for EXPAND_SUGGESTION session command.
769 LOG(ERROR) << "No id found";
770 return false;
771 }
772 UpdateSelectedCandidateIndex();
773 return CommitSuggestionInternal(composer, context, consumed_key_size);
774 }
775
CommitHeadToFocusedSegments(const composer::Composer & composer,const commands::Context & context,size_t * consumed_key_size)776 void SessionConverter::CommitHeadToFocusedSegments(
777 const composer::Composer &composer,
778 const commands::Context &context,
779 size_t *consumed_key_size) {
780 CommitSegmentsInternal(
781 composer, context, segment_index_ + 1, consumed_key_size);
782 }
783
CommitFirstSegment(const composer::Composer & composer,const commands::Context & context,size_t * consumed_key_size)784 void SessionConverter::CommitFirstSegment(
785 const composer::Composer &composer,
786 const commands::Context &context,
787 size_t *consumed_key_size) {
788 CommitSegmentsInternal(composer, context, 1, consumed_key_size);
789 }
790
CommitSegmentsInternal(const composer::Composer & composer,const commands::Context & context,size_t segments_to_commit,size_t * consumed_key_size)791 void SessionConverter::CommitSegmentsInternal(
792 const composer::Composer &composer,
793 const commands::Context &context,
794 size_t segments_to_commit,
795 size_t *consumed_key_size) {
796 DCHECK(CheckState(PREDICTION | CONVERSION));
797 DCHECK(segments_->conversion_segments_size() >= segments_to_commit);
798 ResetResult();
799 candidate_list_visible_ = false;
800 *consumed_key_size = 0;
801
802 // If the number of segments is one, just call Commit.
803 if (segments_->conversion_segments_size() == segments_to_commit) {
804 Commit(composer, context);
805 return;
806 }
807
808 // Store the first conversion segment to the result.
809 if (!UpdateResult(0, segments_to_commit, NULL)) {
810 // If the selected candidate of the first segment has the command
811 // attribute, Cancel is performed instead of Commit.
812 Cancel();
813 ResetState();
814 return;
815 }
816
817 std::vector<size_t> candidate_ids;
818 for (size_t i = 0; i < segments_to_commit; ++i) {
819 // Get the i-th (0 origin) conversion segment and the selected candidate.
820 Segment *segment = segments_->mutable_conversion_segment(i);
821 if (segment == NULL) {
822 LOG(ERROR) << "There is no segment on position " << i;
823 return;
824 }
825
826 // Accumulate the size of i-th segment's key.
827 // The caller will remove corresponding characters from the composer.
828 *consumed_key_size += Util::CharsLen(segment->key());
829
830 // Collect candidate's id for each segment.
831 candidate_ids.push_back(GetCandidateIndexForConverter(i));
832 }
833 converter_->CommitSegments(segments_.get(), candidate_ids);
834
835 // Commit the [0, segments_to_commit - 1] conversion segment.
836 CommitUsageStatsWithSegmentsSize(state_, context, segments_to_commit);
837
838 // Adjust the segment_index, since the [0, segment_to_commit - 1] segments
839 // disappeared.
840 // Note that segment_index_ is unsigned.
841 segment_index_ = segment_index_ > segments_to_commit
842 ? segment_index_ - segments_to_commit : 0;
843 UpdateCandidateList();
844 }
845
CommitPreedit(const composer::Composer & composer,const commands::Context & context)846 void SessionConverter::CommitPreedit(const composer::Composer &composer,
847 const commands::Context &context) {
848 string key, preedit, normalized_preedit;
849 composer.GetQueryForConversion(&key);
850 composer.GetStringForSubmission(&preedit);
851 TextNormalizer::NormalizeText(preedit, &normalized_preedit);
852 SessionOutput::FillPreeditResult(preedit, result_.get());
853
854 ConverterUtil::InitSegmentsFromString(key, normalized_preedit,
855 segments_.get());
856
857 CommitUsageStats(SessionConverterInterface::COMPOSITION, context);
858 ConversionRequest conversion_request(&composer, request_, config_);
859 converter_->FinishConversion(conversion_request, segments_.get());
860 ResetState();
861 }
862
CommitHead(size_t count,const composer::Composer & composer,size_t * consumed_key_size)863 void SessionConverter::CommitHead(
864 size_t count, const composer::Composer &composer,
865 size_t *consumed_key_size) {
866 string preedit;
867 composer.GetStringForSubmission(&preedit);
868 if (count > preedit.length()) {
869 *consumed_key_size = preedit.length();
870 } else {
871 *consumed_key_size = count;
872 }
873 preedit = Util::SubString(preedit, 0, *consumed_key_size);
874 string composition;
875 TextNormalizer::NormalizeText(preedit, &composition);
876 SessionOutput::FillPreeditResult(composition, result_.get());
877 }
878
Revert()879 void SessionConverter::Revert() {
880 converter_->RevertConversion(segments_.get());
881 }
882
SegmentFocusInternal(size_t index)883 void SessionConverter::SegmentFocusInternal(size_t index) {
884 DCHECK(CheckState(PREDICTION | CONVERSION));
885 candidate_list_visible_ = false;
886 if (CheckState(PREDICTION)) {
887 return; // Do nothing.
888 }
889 ResetResult();
890
891 if (segment_index_ == index) {
892 return;
893 }
894
895 SegmentFix();
896 segment_index_ = index;
897 UpdateCandidateList();
898 }
899
SegmentFocusRight()900 void SessionConverter::SegmentFocusRight() {
901 if (segment_index_ + 1 >= segments_->conversion_segments_size()) {
902 // If |segment_index_| is at the tail of the segments,
903 // focus on the head.
904 SegmentFocusLeftEdge();
905 } else {
906 SegmentFocusInternal(segment_index_ + 1);
907 }
908 }
909
SegmentFocusLast()910 void SessionConverter::SegmentFocusLast() {
911 const size_t r_edge = segments_->conversion_segments_size() - 1;
912 SegmentFocusInternal(r_edge);
913 }
914
SegmentFocusLeft()915 void SessionConverter::SegmentFocusLeft() {
916 if (segment_index_ <= 0) {
917 // If |segment_index_| is at the head of the segments,
918 // focus on the tail.
919 SegmentFocusLast();
920 } else {
921 SegmentFocusInternal(segment_index_ - 1);
922 }
923 }
924
SegmentFocusLeftEdge()925 void SessionConverter::SegmentFocusLeftEdge() {
926 SegmentFocusInternal(0);
927 }
928
ResizeSegmentWidth(const composer::Composer & composer,int delta)929 void SessionConverter::ResizeSegmentWidth(const composer::Composer &composer,
930 int delta) {
931 DCHECK(CheckState(PREDICTION | CONVERSION));
932 candidate_list_visible_ = false;
933 if (CheckState(PREDICTION)) {
934 return; // Do nothing.
935 }
936 ResetResult();
937
938 const ConversionRequest conversion_request(&composer, request_, config_);
939 if (!converter_->ResizeSegment(segments_.get(),
940 conversion_request,
941 segment_index_, delta)) {
942 return;
943 }
944
945 UpdateCandidateList();
946 // Clears selected index of a focused segment and trailing segments.
947 // TODO(hsumita): Keep the indices if the segment type is FIXED_VALUE.
948 selected_candidate_indices_.resize(segments_->conversion_segments_size());
949 std::fill(selected_candidate_indices_.begin() + segment_index_ + 1,
950 selected_candidate_indices_.end(), 0);
951 UpdateSelectedCandidateIndex();
952 }
953
SegmentWidthExpand(const composer::Composer & composer)954 void SessionConverter::SegmentWidthExpand(const composer::Composer &composer) {
955 ResizeSegmentWidth(composer, 1);
956 }
957
SegmentWidthShrink(const composer::Composer & composer)958 void SessionConverter::SegmentWidthShrink(const composer::Composer &composer) {
959 ResizeSegmentWidth(composer, -1);
960 }
961
962 const Segment::Candidate *
GetSelectedCandidateOfFocusedSegment() const963 SessionConverter::GetSelectedCandidateOfFocusedSegment() const {
964 if (!candidate_list_->focused()) {
965 return NULL;
966 }
967 const Candidate &cand = candidate_list_->focused_candidate();
968 const Segment &seg = segments_->conversion_segment(segment_index_);
969 return &seg.candidate(cand.id());
970 }
971
CandidateNext(const composer::Composer & composer)972 void SessionConverter::CandidateNext(const composer::Composer &composer) {
973 DCHECK(CheckState(PREDICTION | CONVERSION));
974 ResetResult();
975
976 MaybeExpandPrediction(composer);
977 candidate_list_->MoveNext();
978 candidate_list_visible_ = true;
979 UpdateSelectedCandidateIndex();
980 SegmentFocus();
981 }
982
CandidateNextPage()983 void SessionConverter::CandidateNextPage() {
984 DCHECK(CheckState(PREDICTION | CONVERSION));
985 ResetResult();
986
987 candidate_list_->MoveNextPage();
988 candidate_list_visible_ = true;
989 UpdateSelectedCandidateIndex();
990 SegmentFocus();
991 }
992
CandidatePrev()993 void SessionConverter::CandidatePrev() {
994 DCHECK(CheckState(PREDICTION | CONVERSION));
995 ResetResult();
996
997 candidate_list_->MovePrev();
998 candidate_list_visible_ = true;
999 UpdateSelectedCandidateIndex();
1000 SegmentFocus();
1001 }
1002
CandidatePrevPage()1003 void SessionConverter::CandidatePrevPage() {
1004 DCHECK(CheckState(PREDICTION | CONVERSION));
1005 ResetResult();
1006
1007 candidate_list_->MovePrevPage();
1008 candidate_list_visible_ = true;
1009 UpdateSelectedCandidateIndex();
1010 SegmentFocus();
1011 }
1012
CandidateMoveToId(const int id,const composer::Composer & composer)1013 void SessionConverter::CandidateMoveToId(
1014 const int id, const composer::Composer &composer) {
1015 DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION));
1016 ResetResult();
1017
1018 if (CheckState(SUGGESTION)) {
1019 // This method makes a candidate focused but SUGGESTION state cannot
1020 // have focused candidate.
1021 // To solve this conflict we call Predict() method to transit to
1022 // PREDICTION state, on which existence of focused candidate is acceptable.
1023 Predict(composer);
1024 }
1025 DCHECK(CheckState(PREDICTION | CONVERSION));
1026
1027 candidate_list_->MoveToId(id);
1028 candidate_list_visible_ = false;
1029 UpdateSelectedCandidateIndex();
1030 SegmentFocus();
1031 }
1032
CandidateMoveToPageIndex(const size_t index)1033 void SessionConverter::CandidateMoveToPageIndex(const size_t index) {
1034 DCHECK(CheckState(PREDICTION | CONVERSION));
1035 ResetResult();
1036
1037 candidate_list_->MoveToPageIndex(index);
1038 candidate_list_visible_ = false;
1039 UpdateSelectedCandidateIndex();
1040 SegmentFocus();
1041 }
1042
CandidateMoveToShortcut(const char shortcut)1043 bool SessionConverter::CandidateMoveToShortcut(const char shortcut) {
1044 DCHECK(CheckState(PREDICTION | CONVERSION));
1045
1046 if (!candidate_list_visible_) {
1047 VLOG(1) << "Candidate list is not displayed.";
1048 return false;
1049 }
1050
1051 const string shortcuts(GetCandidateShortcuts(selection_shortcut_));
1052 if (shortcuts.empty()) {
1053 VLOG(1) << "No shortcuts";
1054 return false;
1055 }
1056
1057 // Check if the input character is in the shortcut.
1058 // TODO(komatsu): Support non ASCII characters such as Unicode and
1059 // special keys.
1060 const string::size_type index = shortcuts.find(shortcut);
1061 if (index == string::npos) {
1062 VLOG(1) << "shortcut is not a member of shortcuts.";
1063 return false;
1064 }
1065
1066 if (!candidate_list_->MoveToPageIndex(index)) {
1067 VLOG(1) << "shortcut is out of the range.";
1068 return false;
1069 }
1070 UpdateSelectedCandidateIndex();
1071 ResetResult();
1072 SegmentFocus();
1073 return true;
1074 }
1075
SetCandidateListVisible(bool visible)1076 void SessionConverter::SetCandidateListVisible(bool visible) {
1077 candidate_list_visible_ = visible;
1078 }
1079
PopOutput(const composer::Composer & composer,commands::Output * output)1080 void SessionConverter::PopOutput(
1081 const composer::Composer &composer, commands::Output *output) {
1082 FillOutput(composer, output);
1083 updated_command_ = Segment::Candidate::DEFAULT_COMMAND;
1084 ResetResult();
1085 }
1086
1087 namespace {
MaybeFillConfig(Segment::Candidate::Command command,const config::Config & base_config,commands::Output * output)1088 void MaybeFillConfig(Segment::Candidate::Command command,
1089 const config::Config &base_config,
1090 commands::Output *output) {
1091 if (command == Segment::Candidate::DEFAULT_COMMAND) {
1092 return;
1093 }
1094
1095 *output->mutable_config() = base_config;
1096 switch (command) {
1097 case Segment::Candidate::ENABLE_INCOGNITO_MODE:
1098 output->mutable_config()->set_incognito_mode(true);
1099 break;
1100 case Segment::Candidate::DISABLE_INCOGNITO_MODE:
1101 output->mutable_config()->set_incognito_mode(false);
1102 break;
1103 case Segment::Candidate::ENABLE_PRESENTATION_MODE:
1104 output->mutable_config()->set_presentation_mode(true);
1105 break;
1106 case Segment::Candidate::DISABLE_PRESENTATION_MODE:
1107 output->mutable_config()->set_presentation_mode(false);
1108 break;
1109 default:
1110 LOG(WARNING) << "Unknown command: " << command;
1111 break;
1112 }
1113 }
1114 } // namespace
1115
FillOutput(const composer::Composer & composer,commands::Output * output) const1116 void SessionConverter::FillOutput(
1117 const composer::Composer &composer, commands::Output *output) const {
1118 if (output == NULL) {
1119 LOG(ERROR) << "output is NULL.";
1120 return;
1121 }
1122 if (result_->has_value()) {
1123 FillResult(output->mutable_result());
1124 }
1125 if (CheckState(COMPOSITION)) {
1126 if (!composer.Empty()) {
1127 session::SessionOutput::FillPreedit(composer,
1128 output->mutable_preedit());
1129 }
1130 }
1131
1132 MaybeFillConfig(updated_command_, *config_, output);
1133
1134 if (!IsActive()) {
1135 return;
1136 }
1137
1138 // Composition on Suggestion
1139 if (CheckState(SUGGESTION)) {
1140 // When the suggestion comes from zero query suggestion, the
1141 // composer is empty. In that case, preedit is not rendered.
1142 if (!composer.Empty()) {
1143 session::SessionOutput::FillPreedit(composer,
1144 output->mutable_preedit());
1145 }
1146 } else if (CheckState(PREDICTION | CONVERSION)) {
1147 // Conversion on Prediction or Conversion
1148 FillConversion(output->mutable_preedit());
1149 }
1150 // Candidate list
1151 if (CheckState(SUGGESTION | PREDICTION | CONVERSION) &&
1152 candidate_list_visible_) {
1153 FillCandidates(output->mutable_candidates());
1154 }
1155
1156 // All candidate words
1157 if (CheckState(SUGGESTION | PREDICTION | CONVERSION)) {
1158 FillAllCandidateWords(output->mutable_all_candidate_words());
1159 }
1160 }
1161
1162 // static
SetConversionPreferences(const ConversionPreferences & preferences,Segments * segments)1163 void SessionConverter::SetConversionPreferences(
1164 const ConversionPreferences &preferences,
1165 Segments *segments) {
1166 segments->set_user_history_enabled(preferences.use_history);
1167 segments->set_max_history_segments_size(preferences.max_history_size);
1168 }
1169
Clone() const1170 SessionConverter* SessionConverter::Clone() const {
1171 SessionConverter *session_converter =
1172 new SessionConverter(converter_, request_, config_);
1173
1174 // Copy the members in order of their declarations.
1175 session_converter->state_ = state_;
1176 // TODO(team): copy of |converter_| member.
1177 // We cannot copy the member converter_ from SessionConverterInterface because
1178 // it doesn't (and shouldn't) define a method like GetConverter(). At the
1179 // moment it's ok because the current design guarantees that the converter is
1180 // singleton. However, we should refactor such bad design; see also the
1181 // comment right above.
1182 session_converter->segments_->CopyFrom(*segments_);
1183 session_converter->segment_index_ = segment_index_;
1184 session_converter->previous_suggestions_.CopyFrom(previous_suggestions_);
1185 session_converter->conversion_preferences_ = conversion_preferences();
1186 session_converter->result_->CopyFrom(*result_);
1187 session_converter->request_ = request_;
1188 session_converter->config_ = config_;
1189 session_converter->use_cascading_window_ = use_cascading_window_;
1190 session_converter->selected_candidate_indices_ = selected_candidate_indices_;
1191
1192 if (session_converter->CheckState(SUGGESTION | PREDICTION | CONVERSION)) {
1193 // UpdateCandidateList() is not simple setter and it uses some members.
1194 session_converter->UpdateCandidateList();
1195 session_converter->candidate_list_->MoveToId(candidate_list_->focused_id());
1196 session_converter->SetCandidateListVisible(candidate_list_visible_);
1197 }
1198
1199 return session_converter;
1200 }
1201
ResetResult()1202 void SessionConverter::ResetResult() {
1203 result_->Clear();
1204 }
1205
ResetState()1206 void SessionConverter::ResetState() {
1207 state_ = COMPOSITION;
1208 segment_index_ = 0;
1209 previous_suggestions_.clear();
1210 candidate_list_visible_ = false;
1211 candidate_list_->Clear();
1212 selected_candidate_indices_.clear();
1213 }
1214
SegmentFocus()1215 void SessionConverter::SegmentFocus() {
1216 DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION));
1217 converter_->FocusSegmentValue(segments_.get(),
1218 segment_index_,
1219 GetCandidateIndexForConverter(segment_index_));
1220 }
1221
SegmentFix()1222 void SessionConverter::SegmentFix() {
1223 DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION));
1224 converter_->CommitSegmentValue(segments_.get(),
1225 segment_index_,
1226 GetCandidateIndexForConverter(segment_index_));
1227 }
1228
GetPreedit(const size_t index,const size_t size,string * preedit) const1229 void SessionConverter::GetPreedit(const size_t index,
1230 const size_t size,
1231 string *preedit) const {
1232 DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION));
1233 DCHECK(index + size <= segments_->conversion_segments_size());
1234 DCHECK(preedit);
1235
1236 preedit->clear();
1237 for (size_t i = index; i < size; ++i) {
1238 if (CheckState(CONVERSION)) {
1239 // In conversion mode, all the key of candidates is same.
1240 preedit->append(segments_->conversion_segment(i).key());
1241 } else {
1242 DCHECK(CheckState(SUGGESTION | PREDICTION));
1243 // In suggestion or prediction modes, each key may have
1244 // different keys, so content_key is used although it is
1245 // possibly dropped the conjugational word (ex. the content_key
1246 // of "はしる" is "はし").
1247 preedit->append(GetSelectedCandidate(i).content_key);
1248 }
1249 }
1250 }
1251
GetConversion(const size_t index,const size_t size,string * conversion) const1252 void SessionConverter::GetConversion(const size_t index,
1253 const size_t size,
1254 string *conversion) const {
1255 DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION));
1256 DCHECK(index + size <= segments_->conversion_segments_size());
1257 DCHECK(conversion);
1258
1259 conversion->clear();
1260 for (size_t i = index; i < size; ++i) {
1261 conversion->append(GetSelectedCandidateValue(i));
1262 }
1263 }
1264
GetConsumedPreeditSize(const size_t index,const size_t size) const1265 size_t SessionConverter::GetConsumedPreeditSize(const size_t index,
1266 const size_t size) const {
1267 DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION));
1268 DCHECK(index + size <= segments_->conversion_segments_size());
1269
1270 if (CheckState(SUGGESTION | PREDICTION)) {
1271 DCHECK_EQ(1, size);
1272 const Segment &segment = segments_->conversion_segment(0);
1273 const int id = GetCandidateIndexForConverter(0);
1274 const Segment::Candidate &candidate = segment.candidate(id);
1275 return (candidate.attributes & Segment::Candidate::PARTIALLY_KEY_CONSUMED)
1276 ? candidate.consumed_key_size : kConsumedAllCharacters;
1277 }
1278
1279 DCHECK(CheckState(CONVERSION));
1280 size_t result = 0;
1281 for (size_t i = index; i < size; ++i) {
1282 const int id = GetCandidateIndexForConverter(i);
1283 const Segment::Candidate &candidate =
1284 segments_->conversion_segment(i).candidate(id);
1285 DCHECK(!(candidate.attributes &
1286 Segment::Candidate::PARTIALLY_KEY_CONSUMED));
1287 result += Util::CharsLen(segments_->conversion_segment(i).key());
1288 }
1289 return result;
1290 }
1291
MaybePerformCommandCandidate(const size_t index,const size_t size)1292 bool SessionConverter::MaybePerformCommandCandidate(
1293 const size_t index,
1294 const size_t size) {
1295 // If a candidate has the command attribute, Cancel is performed
1296 // instead of Commit after executing the specified action.
1297 for (size_t i = index; i < size; ++i) {
1298 const int id = GetCandidateIndexForConverter(i);
1299 const Segment::Candidate &candidate =
1300 segments_->conversion_segment(i).candidate(id);
1301 if (candidate.attributes & Segment::Candidate::COMMAND_CANDIDATE) {
1302 switch (candidate.command) {
1303 case Segment::Candidate::DEFAULT_COMMAND:
1304 // Do nothing
1305 break;
1306 case Segment::Candidate::ENABLE_INCOGNITO_MODE:
1307 case Segment::Candidate::DISABLE_INCOGNITO_MODE:
1308 case Segment::Candidate::ENABLE_PRESENTATION_MODE:
1309 case Segment::Candidate::DISABLE_PRESENTATION_MODE:
1310 updated_command_ = candidate.command;
1311 break;
1312 default:
1313 LOG(WARNING) << "Unknown command: " << candidate.command;
1314 break;
1315 }
1316 return true;
1317 }
1318 }
1319 return false;
1320 }
1321
UpdateResult(size_t index,size_t size,size_t * consumed_key_size)1322 bool SessionConverter::UpdateResult(size_t index, size_t size,
1323 size_t *consumed_key_size) {
1324 DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION));
1325
1326 // If command candidate is performed, result is not updated and
1327 // returns false.
1328 if (MaybePerformCommandCandidate(index, size)) {
1329 return false;
1330 }
1331
1332 string preedit, conversion;
1333 GetPreedit(index, size, &preedit);
1334 GetConversion(index, size, &conversion);
1335 if (consumed_key_size) {
1336 *consumed_key_size = GetConsumedPreeditSize(index, size);
1337 }
1338 SessionOutput::FillConversionResult(preedit, conversion, result_.get());
1339 return true;
1340 }
1341
1342 namespace {
1343 // Convert transliteration::TransliterationType to id used in the
1344 // converter. The id number are negative values, and 0 of
1345 // transliteration::TransliterationType is bound for -1 of the id.
GetT13nId(const transliteration::TransliterationType type)1346 int GetT13nId(const transliteration::TransliterationType type) {
1347 return -(type + 1);
1348 }
1349 } // namespace
1350
AppendCandidateList()1351 void SessionConverter::AppendCandidateList() {
1352 DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION));
1353
1354 // Meta candidates are added iff |candidate_list_| is empty.
1355 // This is because if |candidate_list_| is not empty we cannot decide
1356 // where to add meta candidates, especially use_cascading_window flag
1357 // is true (If there are two or more sub candidate lists, and existent
1358 // meta candidates are not located in the same list (distributed over
1359 // some lists), the most appropriate location to be added new meta candidates
1360 // cannot be decided).
1361 const bool add_meta_candidates = (candidate_list_->size() == 0);
1362
1363 const Segment &segment = segments_->conversion_segment(segment_index_);
1364 for (size_t i = candidate_list_->next_available_id();
1365 i < segment.candidates_size();
1366 ++i) {
1367 candidate_list_->AddCandidate(i, segment.candidate(i).value);
1368 // if candidate has spelling correction attribute,
1369 // always display the candidate to let user know the
1370 // miss spelled candidate.
1371 if (i < 10 &&
1372 (segment.candidate(i).attributes &
1373 Segment::Candidate::SPELLING_CORRECTION)) {
1374 candidate_list_visible_ = true;
1375 }
1376 }
1377
1378 const bool focused = (
1379 segments_->request_type() != Segments::SUGGESTION &&
1380 segments_->request_type() != Segments::PARTIAL_SUGGESTION &&
1381 segments_->request_type() != Segments::PARTIAL_PREDICTION);
1382 candidate_list_->set_focused(focused);
1383
1384 if (segment.meta_candidates_size() == 0) {
1385 // For suggestion mode, it is natural that T13N is not initialized.
1386 if (CheckState(SUGGESTION)) {
1387 return;
1388 }
1389 // For other modes, records |segment| just in case.
1390 VLOG(1) << "T13N is not initialized: " << segment.key();
1391 return;
1392 }
1393
1394 if (!add_meta_candidates) {
1395 return;
1396 }
1397
1398 // Set transliteration candidates
1399 CandidateList *transliterations;
1400 if (use_cascading_window_) {
1401 const bool kNoRotate = false;
1402 transliterations = candidate_list_->AllocateSubCandidateList(kNoRotate);
1403 transliterations->set_focused(true);
1404
1405 const char kT13nLabel[] = "そのほかの文字種";
1406 transliterations->set_name(kT13nLabel);
1407 } else {
1408 transliterations = candidate_list_.get();
1409 }
1410
1411 // Add transliterations.
1412 for (size_t i = 0; i < transliteration::NUM_T13N_TYPES; ++i) {
1413 const transliteration::TransliterationType type =
1414 transliteration::TransliterationTypeArray[i];
1415 transliterations->AddCandidateWithAttributes(
1416 GetT13nId(type),
1417 segment.meta_candidate(i).value,
1418 GetT13nAttributes(type));
1419 }
1420 }
1421
UpdateCandidateList()1422 void SessionConverter::UpdateCandidateList() {
1423 DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION));
1424 candidate_list_->Clear();
1425 AppendCandidateList();
1426 }
1427
GetCandidateIndexForConverter(const size_t segment_index) const1428 int SessionConverter::GetCandidateIndexForConverter(
1429 const size_t segment_index) const {
1430 DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION));
1431 // If segment_index does not point to the focused segment, the value
1432 // should be always zero.
1433 if (segment_index != segment_index_) {
1434 return 0;
1435 }
1436 return candidate_list_->focused_id();
1437 }
1438
GetSelectedCandidateValue(const size_t segment_index) const1439 string SessionConverter::GetSelectedCandidateValue(
1440 const size_t segment_index) const {
1441 DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION));
1442 const int id = GetCandidateIndexForConverter(segment_index);
1443 const Segment::Candidate &candidate =
1444 segments_->conversion_segment(segment_index).candidate(id);
1445 if (candidate.attributes & Segment::Candidate::COMMAND_CANDIDATE) {
1446 // Return an empty string, however this path should not be reached.
1447 return "";
1448 }
1449 return candidate.value;
1450 }
1451
GetSelectedCandidate(const size_t segment_index) const1452 const Segment::Candidate &SessionConverter::GetSelectedCandidate(
1453 const size_t segment_index) const {
1454 DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION));
1455 const int id = GetCandidateIndexForConverter(segment_index);
1456 return segments_->conversion_segment(segment_index).candidate(id);
1457 }
1458
FillConversion(commands::Preedit * preedit) const1459 void SessionConverter::FillConversion(commands::Preedit *preedit) const {
1460 DCHECK(CheckState(PREDICTION | CONVERSION));
1461 SessionOutput::FillConversion(*segments_,
1462 segment_index_,
1463 candidate_list_->focused_id(),
1464 preedit);
1465 }
1466
FillResult(commands::Result * result) const1467 void SessionConverter::FillResult(commands::Result *result) const {
1468 result->CopyFrom(*result_);
1469 }
1470
FillCandidates(commands::Candidates * candidates) const1471 void SessionConverter::FillCandidates(commands::Candidates *candidates) const {
1472 DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION));
1473 if (!candidate_list_visible_) {
1474 return;
1475 }
1476
1477 // The position to display the candidate window.
1478 size_t position = 0;
1479 string conversion;
1480 for (size_t i = 0; i < segment_index_; ++i) {
1481 position += Util::CharsLen(GetSelectedCandidate(i).value);
1482 }
1483
1484 // Temporarily added to see if this condition is really satisfied in the
1485 // real world or not.
1486 #ifdef CHANNEL_DEV
1487 CHECK_LT(0, segments_->conversion_segments_size());
1488 #endif // CHANNEL_DEV
1489 const Segment &segment = segments_->conversion_segment(segment_index_);
1490 SessionOutput::FillCandidates(
1491 segment, *candidate_list_, position, candidates);
1492
1493 // Shortcut keys
1494 if (CheckState(PREDICTION | CONVERSION)) {
1495 SessionOutput::FillShortcuts(GetCandidateShortcuts(selection_shortcut_),
1496 candidates);
1497 }
1498
1499 // Store category
1500 switch (segments_->request_type()) {
1501 case Segments::CONVERSION:
1502 candidates->set_category(commands::CONVERSION);
1503 break;
1504 case Segments::PREDICTION:
1505 candidates->set_category(commands::PREDICTION);
1506 break;
1507 case Segments::SUGGESTION:
1508 candidates->set_category(commands::SUGGESTION);
1509 break;
1510 case Segments::PARTIAL_PREDICTION:
1511 // Not PREDICTION because we do not want to get focused candidate.
1512 candidates->set_category(commands::SUGGESTION);
1513 break;
1514 case Segments::PARTIAL_SUGGESTION:
1515 candidates->set_category(commands::SUGGESTION);
1516 break;
1517 default:
1518 LOG(WARNING) << "Unknown request type: " << segments_->request_type();
1519 candidates->set_category(commands::CONVERSION);
1520 break;
1521 }
1522
1523 if (candidates->has_usages()) {
1524 candidates->mutable_usages()->set_category(commands::USAGE);
1525 }
1526 if (candidates->has_subcandidates()) {
1527 // TODO(komatsu): Subcandidate is not always for transliterations.
1528 // The category of the subcandidates should be checked.
1529 candidates->mutable_subcandidates()->set_category(
1530 commands::TRANSLITERATION);
1531 }
1532
1533 // Store display type
1534 candidates->set_display_type(commands::MAIN);
1535 if (candidates->has_usages()) {
1536 candidates->mutable_usages()->set_display_type(commands::CASCADE);
1537 }
1538 if (candidates->has_subcandidates()) {
1539 // TODO(komatsu): Subcandidate is not always for transliterations.
1540 // The category of the subcandidates should be checked.
1541 candidates->mutable_subcandidates()->set_display_type(commands::CASCADE);
1542 }
1543
1544 // Store footer.
1545 SessionOutput::FillFooter(candidates->category(), candidates);
1546 }
1547
1548
FillAllCandidateWords(commands::CandidateList * candidates) const1549 void SessionConverter::FillAllCandidateWords(
1550 commands::CandidateList *candidates) const {
1551 DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION));
1552 commands::Category category;
1553 switch (segments_->request_type()) {
1554 case Segments::CONVERSION:
1555 category = commands::CONVERSION;
1556 break;
1557 case Segments::PREDICTION:
1558 category = commands::PREDICTION;
1559 break;
1560 case Segments::SUGGESTION:
1561 category = commands::SUGGESTION;
1562 break;
1563 case Segments::PARTIAL_PREDICTION:
1564 // Not PREDICTION because we do not want to get focused candidate.
1565 category = commands::SUGGESTION;
1566 break;
1567 case Segments::PARTIAL_SUGGESTION:
1568 category = commands::SUGGESTION;
1569 break;
1570 default:
1571 LOG(WARNING) << "Unknown request type: " << segments_->request_type();
1572 category = commands::CONVERSION;
1573 break;
1574 }
1575
1576 const Segment &segment = segments_->conversion_segment(segment_index_);
1577 SessionOutput::FillAllCandidateWords(
1578 segment, *candidate_list_, category, candidates);
1579 }
1580
SetRequest(const commands::Request * request)1581 void SessionConverter::SetRequest(const commands::Request *request) {
1582 request_ = request;
1583 candidate_list_->set_page_size(request->candidate_page_size());
1584 }
1585
SetConfig(const config::Config * config)1586 void SessionConverter::SetConfig(const config::Config *config) {
1587 config_ = config;
1588 updated_command_ = Segment::Candidate::DEFAULT_COMMAND;
1589 selection_shortcut_ = config->selection_shortcut();
1590 use_cascading_window_ = config->use_cascading_window();
1591 }
1592
OnStartComposition(const commands::Context & context)1593 void SessionConverter::OnStartComposition(const commands::Context &context) {
1594 bool revision_changed = false;
1595 if (context.has_revision()) {
1596 revision_changed = (context.revision() != client_revision_);
1597 client_revision_ = context.revision();
1598 }
1599 if (!context.has_preceding_text()) {
1600 // In this case, reset history segments when the revision is mismatched.
1601 if (revision_changed) {
1602 converter_->ResetConversion(segments_.get());
1603 }
1604 return;
1605 }
1606
1607 const string &preceding_text = context.preceding_text();
1608 // If preceding text is empty, it is OK to reset the history segments by
1609 // calling ResetConversion.
1610 if (preceding_text.empty()) {
1611 converter_->ResetConversion(segments_.get());
1612 return;
1613 }
1614
1615 // Hereafter, we keep the existing history segments as long as it is
1616 // consistent with the preceding text even when revision_changed is true.
1617 string history_text;
1618 for (size_t i = 0; i < segments_->segments_size(); ++i) {
1619 const Segment &segment = segments_->segment(i);
1620 if (segment.segment_type() != Segment::HISTORY) {
1621 break;
1622 }
1623 if (segment.candidates_size() == 0) {
1624 break;
1625 }
1626 history_text.append(segment.candidate(0).value);
1627 }
1628
1629 if (!history_text.empty()) {
1630 // Compare |preceding_text| with |history_text| to check if the history
1631 // segments are still valid or not.
1632 DCHECK(!preceding_text.empty());
1633 DCHECK(!history_text.empty());
1634 if (preceding_text.size() > history_text.size()) {
1635 if (Util::EndsWith(preceding_text, history_text)) {
1636 // History segments seem to be consistent with preceding text.
1637 return;
1638 }
1639 } else {
1640 if (Util::EndsWith(history_text, preceding_text)) {
1641 // History segments seem to be consistent with preceding text.
1642 return;
1643 }
1644 }
1645 }
1646
1647 // Here we reconstruct history segments from |preceding_text| regardless
1648 // of revision mismatch. If it fails the history segments is cleared anyway.
1649 converter_->ReconstructHistory(segments_.get(), preceding_text);
1650 }
1651
UpdateSelectedCandidateIndex()1652 void SessionConverter::UpdateSelectedCandidateIndex() {
1653 int index;
1654 const Candidate &focused_candidate = candidate_list_->focused_candidate();
1655 if (focused_candidate.IsSubcandidateList()) {
1656 const int t13n_index =
1657 focused_candidate.subcandidate_list().focused_index();
1658 index = -1 - t13n_index;
1659 } else {
1660 // TODO(hsumita): Use id instead of focused index.
1661 index = candidate_list_->focused_index();
1662 }
1663 selected_candidate_indices_[segment_index_] = index;
1664 }
1665
InitializeSelectedCandidateIndices()1666 void SessionConverter::InitializeSelectedCandidateIndices() {
1667 selected_candidate_indices_.clear();
1668 selected_candidate_indices_.resize(segments_->conversion_segments_size());
1669 }
1670
UpdateCandidateStats(const string & base_name,int32 index)1671 void SessionConverter::UpdateCandidateStats(const string &base_name,
1672 int32 index) {
1673 string prefix;
1674 if (index < 0) {
1675 prefix = "TransliterationCandidates";
1676 index = -1 - index;
1677 } else {
1678 prefix = base_name + "Candidates";
1679 }
1680
1681 if (index <= 9) {
1682 const string stats_name = prefix + std::to_string(index);
1683 UsageStats::IncrementCount(stats_name);
1684 } else {
1685 const string stats_name = prefix + "GE10";
1686 UsageStats::IncrementCount(stats_name);
1687 }
1688 }
1689
CommitUsageStats(SessionConverterInterface::State commit_state,const commands::Context & context)1690 void SessionConverter::CommitUsageStats(
1691 SessionConverterInterface::State commit_state,
1692 const commands::Context &context) {
1693 size_t commit_segment_size = 0;
1694 switch (commit_state) {
1695 case COMPOSITION:
1696 commit_segment_size = 0;
1697 break;
1698 case SUGGESTION:
1699 case PREDICTION:
1700 commit_segment_size = 1;
1701 break;
1702 case CONVERSION:
1703 commit_segment_size = segments_->conversion_segments_size();
1704 break;
1705 default:
1706 LOG(DFATAL) << "Unexpected state: " << commit_state;
1707 }
1708 CommitUsageStatsWithSegmentsSize(commit_state, context, commit_segment_size);
1709 }
1710
CommitUsageStatsWithSegmentsSize(SessionConverterInterface::State commit_state,const commands::Context & context,size_t commit_segments_size)1711 void SessionConverter::CommitUsageStatsWithSegmentsSize(
1712 SessionConverterInterface::State commit_state,
1713 const commands::Context &context,
1714 size_t commit_segments_size) {
1715 CHECK_LE(commit_segments_size, selected_candidate_indices_.size());
1716
1717 string stats_str;
1718 switch (commit_state) {
1719 case COMPOSITION:
1720 stats_str = "Composition";
1721 break;
1722 case SUGGESTION:
1723 case PREDICTION:
1724 // Suggestion related usage stats are collected as Prediction.
1725 stats_str = "Prediction";
1726 UpdateCandidateStats(stats_str, selected_candidate_indices_[0]);
1727 break;
1728 case CONVERSION:
1729 stats_str = "Conversion";
1730 for (size_t i = 0; i < commit_segments_size; ++i) {
1731 UpdateCandidateStats(stats_str,
1732 selected_candidate_indices_[i]);
1733 }
1734 break;
1735 default:
1736 LOG(DFATAL) << "Unexpected state: " << commit_state;
1737 stats_str = "Unknown";
1738 }
1739
1740 UsageStats::IncrementCount("Commit");
1741 UsageStats::IncrementCount("CommitFrom" + stats_str);
1742
1743 if (stats_str != "Unknown") {
1744 if (SessionUsageStatsUtil::HasExperimentalFeature(context,
1745 "chrome_omnibox")) {
1746 UsageStats::IncrementCount("CommitFrom" + stats_str + "InChromeOmnibox");
1747 }
1748 if (SessionUsageStatsUtil::HasExperimentalFeature(context,
1749 "google_search_box")) {
1750 UsageStats::IncrementCount(
1751 "CommitFrom" + stats_str + "InGoogleSearchBox");
1752 }
1753 }
1754
1755 const std::vector<int>::iterator it = selected_candidate_indices_.begin();
1756 selected_candidate_indices_.erase(it, it + commit_segments_size);
1757 }
1758
1759 } // namespace session
1760 } // namespace mozc
1761