blob: d2f3759023ae5891188b02bf23b94244096d40c8 [file] [log] [blame]
// Copyright 2010-2015, 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.
// Session class of Mozc server.
#include "session/session.h"
#include <string>
#include <vector>
#include "base/logging.h"
#include "base/port.h"
#include "base/process.h"
#include "base/singleton.h"
#include "base/url.h"
#include "base/util.h"
#include "base/version.h"
#include "composer/composer.h"
#include "composer/table.h"
#include "config/config.pb.h"
#include "config/config_handler.h"
#include "engine/engine_interface.h"
#include "engine/user_data_manager_interface.h"
#include "session/commands.pb.h"
#include "session/internal/ime_context.h"
#include "session/internal/key_event_transformer.h"
#include "session/internal/keymap-inl.h"
#include "session/internal/keymap.h"
#include "session/internal/keymap_factory.h"
#include "session/internal/session_output.h"
#include "session/key_event_util.h"
#include "session/session_converter.h"
#include "session/session_usage_stats_util.h"
#include "usage_stats/usage_stats.h"
using mozc::usage_stats::UsageStats;
namespace mozc {
namespace session {
namespace {
// Set input mode if the current input mode is not the given mode.
void SwitchInputMode(const transliteration::TransliterationType mode,
composer::Composer *composer) {
if (composer->GetInputMode() != mode) {
composer->SetInputMode(mode);
}
composer->SetNewInput();
}
// Set input mode to the |composer| if the the input mode of |composer| is not
// the given |mode|.
void ApplyInputMode(const commands::CompositionMode mode,
composer::Composer *composer) {
switch (mode) {
case commands::HIRAGANA:
SwitchInputMode(transliteration::HIRAGANA, composer);
break;
case commands::FULL_KATAKANA:
SwitchInputMode(transliteration::FULL_KATAKANA, composer);
break;
case commands::HALF_KATAKANA:
SwitchInputMode(transliteration::HALF_KATAKANA, composer);
break;
case commands::FULL_ASCII:
SwitchInputMode(transliteration::FULL_ASCII, composer);
break;
case commands::HALF_ASCII:
SwitchInputMode(transliteration::HALF_ASCII, composer);
break;
default:
LOG(DFATAL) << "ime on with invalid mode";
}
}
// Return true if the specified key event consists of any modifier key only.
bool IsPureModifierKeyEvent(const commands::KeyEvent &key) {
if (key.has_key_code()) {
return false;
}
if (key.has_special_key()) {
return false;
}
if (key.modifier_keys_size() == 0) {
return false;
}
return true;
}
bool IsPureSpaceKey(const commands::KeyEvent &key) {
if (key.has_key_code()) {
return false;
}
if (key.modifier_keys_size() > 0) {
return false;
}
if (!key.has_special_key()) {
return false;
}
if (key.special_key() != commands::KeyEvent::SPACE) {
return false;
}
return true;
}
// Set session state to the given state and also update related status.
void SetSessionState(const ImeContext::State state, ImeContext *context) {
const ImeContext::State prev_state = context->state();
context->set_state(state);
switch (state) {
case ImeContext::DIRECT:
case ImeContext::PRECOMPOSITION:
context->mutable_composer()->Reset();
break;
case ImeContext::CONVERSION:
context->mutable_composer()->ResetInputMode();
break;
case ImeContext::COMPOSITION:
if (prev_state == ImeContext::PRECOMPOSITION) {
// NOTE: In case of state change including commitment, state change
// doesn't happen directly at once from CONVERSION to COMPOSITION.
// Actual state change is CONVERSION to PRECOMPOSITION at first,
// followed by PRECOMPOSITION to COMPOSITION.
// However in this case we can only get one SendCaretRectangle
// because the state change is executed atomically.
context->mutable_composition_rectangle()->CopyFrom(
context->caret_rectangle());
// Notify the start of composition to the converter so that internal
// state can be refreshed by the client context (especially by
// preceding text).
context->mutable_converter()->OnStartComposition(
context->client_context());
}
break;
default:
// Do nothing.
break;
}
}
commands::CompositionMode ToCompositionMode(
mozc::transliteration::TransliterationType type) {
commands::CompositionMode mode = commands::HIRAGANA;
switch (type) {
case transliteration::HIRAGANA:
mode = commands::HIRAGANA;
break;
case transliteration::FULL_KATAKANA:
mode = commands::FULL_KATAKANA;
break;
case transliteration::HALF_KATAKANA:
mode = commands::HALF_KATAKANA;
break;
case transliteration::FULL_ASCII:
mode = commands::FULL_ASCII;
break;
case transliteration::HALF_ASCII:
mode = commands::HALF_ASCII;
break;
default:
LOG(ERROR) << "Unknown input mode: " << type;
// use HIRAGANA as a default.
}
return mode;
}
ImeContext::State GetEffectiveStateForTestSendKey(
const commands::KeyEvent &key,
ImeContext::State state) {
if (!key.has_activated()) {
return state;
}
if (state == ImeContext::DIRECT && key.activated()) {
// Indirect IME On found.
return ImeContext::PRECOMPOSITION;
}
if (state != ImeContext::DIRECT && !key.activated()) {
// Indirect IME Off found.
return ImeContext::DIRECT;
}
return state;
}
} // namespace
// TODO(komatsu): Remove these argument by using/making singletons.
Session::Session(EngineInterface *engine)
: engine_(engine), context_(new ImeContext) {
InitContext(context_.get());
}
Session::~Session() {}
void Session::InitContext(ImeContext *context) const {
context->set_create_time(Util::GetTime());
context->set_last_command_time(0);
context->set_composer(new composer::Composer(NULL, &context->GetRequest()));
context->set_converter(
new SessionConverter(engine_->GetConverter(), &context->GetRequest()));
#ifdef OS_WIN
// On Windows session is started with direct mode.
// FIXME(toshiyuki): Ditto for Mac after verifying on Mac.
context->set_state(ImeContext::DIRECT);
#else
context->set_state(ImeContext::PRECOMPOSITION);
#endif
context->mutable_client_context()->Clear();
UpdateConfig(config::ConfigHandler::GetConfig(), context);
}
void Session::PushUndoContext() {
// TODO(komatsu): Support multiple undo.
prev_context_.reset(new ImeContext);
InitContext(prev_context_.get());
ImeContext::CopyContext(*context_, prev_context_.get());
}
void Session::PopUndoContext() {
// TODO(komatsu): Support multiple undo.
if (!prev_context_.get()) {
return;
}
context_.swap(prev_context_);
prev_context_.reset(NULL);
}
void Session::ClearUndoContext() {
prev_context_.reset(NULL);
}
void Session::EnsureIMEIsOn() {
if (context_->state() == ImeContext::DIRECT) {
SetSessionState(ImeContext::PRECOMPOSITION, context_.get());
}
}
bool Session::SendCommand(commands::Command *command) {
UpdateTime();
UpdatePreferences(command);
if (!command->input().has_command()) {
return false;
}
TransformInput(command->mutable_input());
SessionUsageStatsUtil::AddSendCommandInputStats(command->input());
const commands::SessionCommand &session_command = command->input().command();
bool result = false;
if (session_command.type() == commands::SessionCommand::SWITCH_INPUT_MODE) {
if (!session_command.has_composition_mode()) {
return false;
}
switch (session_command.composition_mode()) {
case commands::DIRECT:
// TODO(komatsu): Implement here.
break;
case commands::HIRAGANA:
result = InputModeHiragana(command);
break;
case commands::FULL_KATAKANA:
result = InputModeFullKatakana(command);
break;
case commands::HALF_ASCII:
result = InputModeHalfASCII(command);
break;
case commands::FULL_ASCII:
result = InputModeFullASCII(command);
break;
case commands::HALF_KATAKANA:
result = InputModeHalfKatakana(command);
break;
default:
LOG(ERROR) << "Unknown mode: " << session_command.composition_mode();
break;
}
return result;
}
DCHECK_EQ(false, result);
switch (command->input().command().type()) {
case commands::SessionCommand::REVERT:
result = Revert(command);
break;
case commands::SessionCommand::SUBMIT:
result = Commit(command);
break;
case commands::SessionCommand::SELECT_CANDIDATE:
result = SelectCandidate(command);
break;
case commands::SessionCommand::SUBMIT_CANDIDATE:
result = CommitCandidate(command);
break;
case commands::SessionCommand::HIGHLIGHT_CANDIDATE:
result = HighlightCandidate(command);
break;
case commands::SessionCommand::GET_STATUS:
result = GetStatus(command);
break;
case commands::SessionCommand::CONVERT_REVERSE:
result = ConvertReverse(command);
break;
case commands::SessionCommand::UNDO:
result = Undo(command);
break;
case commands::SessionCommand::RESET_CONTEXT:
result = ResetContext(command);
break;
case commands::SessionCommand::MOVE_CURSOR:
result = MoveCursorTo(command);
break;
case commands::SessionCommand::EXPAND_SUGGESTION:
result = ExpandSuggestion(command);
break;
case commands::SessionCommand::SWITCH_INPUT_FIELD_TYPE:
result = SwitchInputFieldType(command);
break;
case commands::SessionCommand::USAGE_STATS_EVENT:
// Set consumed to false, because the client don't need to do anything
// when it receive the output from the server.
command->mutable_output()->set_consumed(false);
result = true;
break;
case commands::SessionCommand::UNDO_OR_REWIND:
result = UndoOrRewind(command);
break;
case commands::SessionCommand::SEND_CARET_LOCATION:
result = SetCaretLocation(command);
break;
case commands::SessionCommand::COMMIT_RAW_TEXT:
result = CommitRawText(command);
break;
case commands::SessionCommand::CONVERT_PREV_PAGE:
result = ConvertPrevPage(command);
break;
case commands::SessionCommand::CONVERT_NEXT_PAGE:
result = ConvertNextPage(command);
break;
case commands::SessionCommand::TURN_ON_IME:
result = MakeSureIMEOn(command);
break;
case commands::SessionCommand::TURN_OFF_IME:
result = MakeSureIMEOff(command);
break;
default:
LOG(WARNING) << "Unknown command" << command->DebugString();
result = DoNothing(command);
break;
}
return result;
}
bool Session::TestSendKey(commands::Command *command) {
UpdateTime();
UpdatePreferences(command);
TransformInput(command->mutable_input());
if (context_->state() == ImeContext::NONE) {
// This must be an error.
LOG(ERROR) << "Invalid state: NONE";
return false;
}
const commands::KeyEvent &key = command->input().key();
// To support indirect IME on/off by using KeyEvent::activated, use effective
// state instead of directly using context_->state().
const ImeContext::State state = GetEffectiveStateForTestSendKey(
key, context_->state());
const keymap::KeyMapManager *keymap =
keymap::KeyMapFactory::GetKeyMapManager(context_->keymap());
// Direct input
if (state == ImeContext::DIRECT) {
keymap::DirectInputState::Commands key_command;
if (!keymap->GetCommandDirect(key, &key_command) ||
key_command == keymap::DirectInputState::NONE) {
return EchoBack(command);
}
return DoNothing(command);
}
// Precomposition
if (state == ImeContext::PRECOMPOSITION) {
keymap::PrecompositionState::Commands key_command;
const bool is_suggestion =
context_->converter().CheckState(SessionConverterInterface::SUGGESTION);
const bool result = is_suggestion
? keymap->GetCommandZeroQuerySuggestion(key, &key_command)
: keymap->GetCommandPrecomposition(key, &key_command);
if (!result || key_command == keymap::PrecompositionState::NONE) {
// Clear undo context just in case. b/5529702.
// Note that the undo context will not be cleared in
// EchoBackAndClearUndoContext if the key event consists of modifier keys
// only.
return EchoBackAndClearUndoContext(command);
}
// If the input_style is DIRECT_INPUT, KeyEvent is not consumed
// and done echo back. It works only when key_string is equal to
// key_code. We should fix this limitation when the as_is flag is
// used for rather than numpad characters.
if (key_command == keymap::PrecompositionState::INSERT_CHARACTER &&
key.input_style() == commands::KeyEvent::DIRECT_INPUT) {
return EchoBack(command);
}
// TODO(komatsu): This is a hack to work around the problem with
// the inconsistency between TestSendKey and SendKey.
switch (key_command) {
case keymap::PrecompositionState::INSERT_SPACE:
if (!IsFullWidthInsertSpace(command->input()) && IsPureSpaceKey(key)) {
return EchoBackAndClearUndoContext(command);
}
return DoNothing(command);
case keymap::PrecompositionState::INSERT_ALTERNATE_SPACE:
if (IsFullWidthInsertSpace(command->input()) && IsPureSpaceKey(key)) {
return EchoBackAndClearUndoContext(command);
}
return DoNothing(command);
case keymap::PrecompositionState::INSERT_HALF_SPACE:
if (IsPureSpaceKey(key)) {
return EchoBackAndClearUndoContext(command);
}
return DoNothing(command);
case keymap::PrecompositionState::INSERT_FULL_SPACE:
return DoNothing(command);
default:
// Do nothing.
break;
}
if (key_command == keymap::PrecompositionState::REVERT) {
return Revert(command);
}
// If undo context is empty, echoes back the key event so that it can be
// handled by the application. b/5553298
if (key_command == keymap::PrecompositionState::UNDO &&
!prev_context_.get()) {
return EchoBack(command);
}
return DoNothing(command);
}
// Do nothing.
return DoNothing(command);
}
bool Session::SendKey(commands::Command *command) {
UpdateTime();
UpdatePreferences(command);
TransformInput(command->mutable_input());
// To support indirect IME on/off by using KeyEvent::activated, use effective
// state instead of directly using context_->state().
HandleIndirectImeOnOff(command);
SessionUsageStatsUtil::AddSendKeyInputStats(command->input());
bool result = false;
switch (context_->state()) {
case ImeContext::DIRECT:
result = SendKeyDirectInputState(command);
break;
case ImeContext::PRECOMPOSITION:
result = SendKeyPrecompositionState(command);
break;
case ImeContext::COMPOSITION:
result = SendKeyCompositionState(command);
break;
case ImeContext::CONVERSION:
result = SendKeyConversionState(command);
break;
case ImeContext::NONE:
result = false;
break;
}
SessionUsageStatsUtil::AddSendKeyOutputStats(command->output());
return result;
}
bool Session::SendKeyDirectInputState(commands::Command *command) {
keymap::DirectInputState::Commands key_command;
const keymap::KeyMapManager *keymap =
keymap::KeyMapFactory::GetKeyMapManager(context_->keymap());
if (!keymap->GetCommandDirect(command->input().key(), &key_command)) {
return EchoBackAndClearUndoContext(command);
}
string command_name;
if (keymap->GetNameFromCommandDirect(key_command, &command_name)) {
UsageStats::IncrementCount("Performed_Direct_" + command_name);
}
switch (key_command) {
case keymap::DirectInputState::IME_ON:
return IMEOn(command);
case keymap::DirectInputState::INPUT_MODE_HIRAGANA:
return InputModeHiragana(command);
case keymap::DirectInputState::INPUT_MODE_FULL_KATAKANA:
return InputModeFullKatakana(command);
case keymap::DirectInputState::INPUT_MODE_HALF_KATAKANA:
return InputModeHalfKatakana(command);
case keymap::DirectInputState::INPUT_MODE_FULL_ALPHANUMERIC:
return InputModeFullASCII(command);
case keymap::DirectInputState::INPUT_MODE_HALF_ALPHANUMERIC:
return InputModeHalfASCII(command);
case keymap::DirectInputState::NONE:
return EchoBackAndClearUndoContext(command);
case keymap::DirectInputState::RECONVERT:
return RequestConvertReverse(command);
}
return false;
}
bool Session::SendKeyPrecompositionState(commands::Command *command) {
keymap::PrecompositionState::Commands key_command;
const keymap::KeyMapManager *keymap =
keymap::KeyMapFactory::GetKeyMapManager(context_->keymap());
const bool result =
context_->converter().CheckState(SessionConverterInterface::SUGGESTION) ?
keymap->GetCommandZeroQuerySuggestion(command->input().key(),
&key_command) :
keymap->GetCommandPrecomposition(command->input().key(), &key_command);
if (!result) {
return EchoBackAndClearUndoContext(command);
}
string command_name;
if (keymap->GetNameFromCommandPrecomposition(key_command, &command_name)) {
UsageStats::IncrementCount("Performed_Precomposition_" + command_name);
}
// Update the client context (if any) for later use. Note that the client
// context is updated only here. In other words, we will stop updating the
// client context once a conversion starts (mainly for performance reasons).
if (command->has_input() && command->input().has_context()) {
context_->mutable_client_context()->CopyFrom(
command->input().context());
} else {
context_->mutable_client_context()->Clear();
}
switch (key_command) {
case keymap::PrecompositionState::INSERT_CHARACTER:
return InsertCharacter(command);
case keymap::PrecompositionState::INSERT_SPACE:
return InsertSpace(command);
case keymap::PrecompositionState::INSERT_ALTERNATE_SPACE:
return InsertSpaceToggled(command);
case keymap::PrecompositionState::INSERT_HALF_SPACE:
return InsertSpaceHalfWidth(command);
case keymap::PrecompositionState::INSERT_FULL_SPACE:
return InsertSpaceFullWidth(command);
case keymap::PrecompositionState::TOGGLE_ALPHANUMERIC_MODE:
return ToggleAlphanumericMode(command);
case keymap::PrecompositionState::REVERT:
return Revert(command);
case keymap::PrecompositionState::UNDO:
return RequestUndo(command);
case keymap::PrecompositionState::IME_OFF:
return IMEOff(command);
case keymap::PrecompositionState::IME_ON:
return DoNothing(command);
case keymap::PrecompositionState::INPUT_MODE_HIRAGANA:
return InputModeHiragana(command);
case keymap::PrecompositionState::INPUT_MODE_FULL_KATAKANA:
return InputModeFullKatakana(command);
case keymap::PrecompositionState::INPUT_MODE_HALF_KATAKANA:
return InputModeHalfKatakana(command);
case keymap::PrecompositionState::INPUT_MODE_FULL_ALPHANUMERIC:
return InputModeFullASCII(command);
case keymap::PrecompositionState::INPUT_MODE_HALF_ALPHANUMERIC:
return InputModeHalfASCII(command);
case keymap::PrecompositionState::INPUT_MODE_SWITCH_KANA_TYPE:
return InputModeSwitchKanaType(command);
case keymap::PrecompositionState::LAUNCH_CONFIG_DIALOG:
return LaunchConfigDialog(command);
case keymap::PrecompositionState::LAUNCH_DICTIONARY_TOOL:
return LaunchDictionaryTool(command);
case keymap::PrecompositionState::LAUNCH_WORD_REGISTER_DIALOG:
return LaunchWordRegisterDialog(command);
// For zero query suggestion
case keymap::PrecompositionState::CANCEL:
// It is a little kind of abuse of the EditCancel command. It
// would be nice to make a new command when EditCancel is
// extended or the requirement of this command is added.
return EditCancel(command);
case keymap::PrecompositionState::CANCEL_AND_IME_OFF:
// The same to keymap::PrecompositionState::CANCEL.
return EditCancelAndIMEOff(command);
// For zero query suggestion
case keymap::PrecompositionState::COMMIT_FIRST_SUGGESTION:
return CommitFirstSuggestion(command);
// For zero query suggestion
case keymap::PrecompositionState::PREDICT_AND_CONVERT:
return PredictAndConvert(command);
case keymap::PrecompositionState::NONE:
return EchoBackAndClearUndoContext(command);
case keymap::PrecompositionState::RECONVERT:
return RequestConvertReverse(command);
}
return false;
}
bool Session::SendKeyCompositionState(commands::Command *command) {
keymap::CompositionState::Commands key_command;
const keymap::KeyMapManager *keymap =
keymap::KeyMapFactory::GetKeyMapManager(context_->keymap());
const bool result =
context_->converter().CheckState(SessionConverterInterface::SUGGESTION) ?
keymap->GetCommandSuggestion(command->input().key(), &key_command) :
keymap->GetCommandComposition(command->input().key(), &key_command);
if (!result) {
return DoNothing(command);
}
string command_name;
if (keymap->GetNameFromCommandComposition(key_command, &command_name)) {
UsageStats::IncrementCount("Performed_Composition_" + command_name);
}
switch (key_command) {
case keymap::CompositionState::INSERT_CHARACTER:
return InsertCharacter(command);
case keymap::CompositionState::COMMIT:
return Commit(command);
case keymap::CompositionState::COMMIT_FIRST_SUGGESTION:
return CommitFirstSuggestion(command);
case keymap::CompositionState::CONVERT:
return Convert(command);
case keymap::CompositionState::CONVERT_WITHOUT_HISTORY:
return ConvertWithoutHistory(command);
case keymap::CompositionState::PREDICT_AND_CONVERT:
return PredictAndConvert(command);
case keymap::CompositionState::DEL:
return Delete(command);
case keymap::CompositionState::BACKSPACE:
return Backspace(command);
case keymap::CompositionState::INSERT_SPACE:
return InsertSpace(command);
case keymap::CompositionState::INSERT_ALTERNATE_SPACE:
return InsertSpaceToggled(command);
case keymap::CompositionState::INSERT_HALF_SPACE:
return InsertSpaceHalfWidth(command);
case keymap::CompositionState::INSERT_FULL_SPACE:
return InsertSpaceFullWidth(command);
case keymap::CompositionState::MOVE_CURSOR_LEFT:
return MoveCursorLeft(command);
case keymap::CompositionState::MOVE_CURSOR_RIGHT:
return MoveCursorRight(command);
case keymap::CompositionState::MOVE_CURSOR_TO_BEGINNING:
return MoveCursorToBeginning(command);
case keymap::CompositionState::MOVE_MOVE_CURSOR_TO_END:
return MoveCursorToEnd(command);
case keymap::CompositionState::CANCEL:
return EditCancel(command);
case keymap::CompositionState::CANCEL_AND_IME_OFF:
return EditCancelAndIMEOff(command);
case keymap::CompositionState::UNDO:
return RequestUndo(command);
case keymap::CompositionState::IME_OFF:
return IMEOff(command);
case keymap::CompositionState::IME_ON:
return DoNothing(command);
case keymap::CompositionState::CONVERT_TO_HIRAGANA:
return ConvertToHiragana(command);
case keymap::CompositionState::CONVERT_TO_FULL_KATAKANA:
return ConvertToFullKatakana(command);
case keymap::CompositionState::CONVERT_TO_HALF_KATAKANA:
return ConvertToHalfKatakana(command);
case keymap::CompositionState::CONVERT_TO_HALF_WIDTH:
return ConvertToHalfWidth(command);
case keymap::CompositionState::CONVERT_TO_FULL_ALPHANUMERIC:
return ConvertToFullASCII(command);
case keymap::CompositionState::CONVERT_TO_HALF_ALPHANUMERIC:
return ConvertToHalfASCII(command);
case keymap::CompositionState::SWITCH_KANA_TYPE:
return SwitchKanaType(command);
case keymap::CompositionState::DISPLAY_AS_HIRAGANA:
return DisplayAsHiragana(command);
case keymap::CompositionState::DISPLAY_AS_FULL_KATAKANA:
return DisplayAsFullKatakana(command);
case keymap::CompositionState::DISPLAY_AS_HALF_KATAKANA:
return DisplayAsHalfKatakana(command);
case keymap::CompositionState::TRANSLATE_HALF_WIDTH:
return TranslateHalfWidth(command);
case keymap::CompositionState::TRANSLATE_FULL_ASCII:
return TranslateFullASCII(command);
case keymap::CompositionState::TRANSLATE_HALF_ASCII:
return TranslateHalfASCII(command);
case keymap::CompositionState::TOGGLE_ALPHANUMERIC_MODE:
return ToggleAlphanumericMode(command);
case keymap::CompositionState::INPUT_MODE_HIRAGANA:
return InputModeHiragana(command);
case keymap::CompositionState::INPUT_MODE_FULL_KATAKANA:
return InputModeFullKatakana(command);
case keymap::CompositionState::INPUT_MODE_HALF_KATAKANA:
return InputModeHalfKatakana(command);
case keymap::CompositionState::INPUT_MODE_FULL_ALPHANUMERIC:
return InputModeFullASCII(command);
case keymap::CompositionState::INPUT_MODE_HALF_ALPHANUMERIC:
return InputModeHalfASCII(command);
case keymap::CompositionState::NONE:
return DoNothing(command);
}
return false;
}
bool Session::SendKeyConversionState(commands::Command *command) {
keymap::ConversionState::Commands key_command;
const keymap::KeyMapManager *keymap =
keymap::KeyMapFactory::GetKeyMapManager(context_->keymap());
const bool result =
context_->converter().CheckState(SessionConverterInterface::PREDICTION) ?
keymap->GetCommandPrediction(command->input().key(), &key_command) :
keymap->GetCommandConversion(command->input().key(), &key_command);
if (!result) {
return DoNothing(command);
}
string command_name;
if (keymap->GetNameFromCommandConversion(key_command,
&command_name)) {
UsageStats::IncrementCount("Performed_Conversion_" + command_name);
}
switch (key_command) {
case keymap::ConversionState::INSERT_CHARACTER:
return InsertCharacter(command);
case keymap::ConversionState::INSERT_SPACE:
return InsertSpace(command);
case keymap::ConversionState::INSERT_ALTERNATE_SPACE:
return InsertSpaceToggled(command);
case keymap::ConversionState::INSERT_HALF_SPACE:
return InsertSpaceHalfWidth(command);
case keymap::ConversionState::INSERT_FULL_SPACE:
return InsertSpaceFullWidth(command);
case keymap::ConversionState::COMMIT:
return Commit(command);
case keymap::ConversionState::COMMIT_SEGMENT:
return CommitSegment(command);
case keymap::ConversionState::CONVERT_NEXT:
return ConvertNext(command);
case keymap::ConversionState::CONVERT_PREV:
return ConvertPrev(command);
case keymap::ConversionState::CONVERT_NEXT_PAGE:
return ConvertNextPage(command);
case keymap::ConversionState::CONVERT_PREV_PAGE:
return ConvertPrevPage(command);
case keymap::ConversionState::PREDICT_AND_CONVERT:
return PredictAndConvert(command);
case keymap::ConversionState::SEGMENT_FOCUS_LEFT:
return SegmentFocusLeft(command);
case keymap::ConversionState::SEGMENT_FOCUS_RIGHT:
return SegmentFocusRight(command);
case keymap::ConversionState::SEGMENT_FOCUS_FIRST:
return SegmentFocusLeftEdge(command);
case keymap::ConversionState::SEGMENT_FOCUS_LAST:
return SegmentFocusLast(command);
case keymap::ConversionState::SEGMENT_WIDTH_EXPAND:
return SegmentWidthExpand(command);
case keymap::ConversionState::SEGMENT_WIDTH_SHRINK:
return SegmentWidthShrink(command);
case keymap::ConversionState::CANCEL:
return ConvertCancel(command);
case keymap::ConversionState::CANCEL_AND_IME_OFF:
return EditCancelAndIMEOff(command);
case keymap::ConversionState::UNDO:
return RequestUndo(command);
case keymap::ConversionState::IME_OFF:
return IMEOff(command);
case keymap::ConversionState::IME_ON:
return DoNothing(command);
case keymap::ConversionState::CONVERT_TO_HIRAGANA:
return ConvertToHiragana(command);
case keymap::ConversionState::CONVERT_TO_FULL_KATAKANA:
return ConvertToFullKatakana(command);
case keymap::ConversionState::CONVERT_TO_HALF_KATAKANA:
return ConvertToHalfKatakana(command);
case keymap::ConversionState::CONVERT_TO_HALF_WIDTH:
return ConvertToHalfWidth(command);
case keymap::ConversionState::CONVERT_TO_FULL_ALPHANUMERIC:
return ConvertToFullASCII(command);
case keymap::ConversionState::CONVERT_TO_HALF_ALPHANUMERIC:
return ConvertToHalfASCII(command);
case keymap::ConversionState::SWITCH_KANA_TYPE:
return SwitchKanaType(command);
case keymap::ConversionState::DISPLAY_AS_HIRAGANA:
return DisplayAsHiragana(command);
case keymap::ConversionState::DISPLAY_AS_FULL_KATAKANA:
return DisplayAsFullKatakana(command);
case keymap::ConversionState::DISPLAY_AS_HALF_KATAKANA:
return DisplayAsHalfKatakana(command);
case keymap::ConversionState::TRANSLATE_HALF_WIDTH:
return TranslateHalfWidth(command);
case keymap::ConversionState::TRANSLATE_FULL_ASCII:
return TranslateFullASCII(command);
case keymap::ConversionState::TRANSLATE_HALF_ASCII:
return TranslateHalfASCII(command);
case keymap::ConversionState::TOGGLE_ALPHANUMERIC_MODE:
return ToggleAlphanumericMode(command);
case keymap::ConversionState::INPUT_MODE_HIRAGANA:
return InputModeHiragana(command);
case keymap::ConversionState::INPUT_MODE_FULL_KATAKANA:
return InputModeFullKatakana(command);
case keymap::ConversionState::INPUT_MODE_HALF_KATAKANA:
return InputModeHalfKatakana(command);
case keymap::ConversionState::INPUT_MODE_FULL_ALPHANUMERIC:
return InputModeFullASCII(command);
case keymap::ConversionState::INPUT_MODE_HALF_ALPHANUMERIC:
return InputModeHalfASCII(command);
case keymap::ConversionState::REPORT_BUG:
return ReportBug(command);
case keymap::ConversionState::DELETE_SELECTED_CANDIDATE:
return DeleteSelectedCandidateFromHistory(command);
case keymap::ConversionState::NONE:
return DoNothing(command);
}
return false;
}
void Session::UpdatePreferences(commands::Command *command) {
DCHECK(command);
const config::Config &config = command->input().config();
if (config.has_session_keymap()) {
context_->set_keymap(config.session_keymap());
} else {
context_->set_keymap(GET_CONFIG(session_keymap));
}
if (command->input().has_capability()) {
context_->mutable_client_capability()->CopyFrom(
command->input().capability());
}
UpdateOperationPreferences(config, context_.get());
}
bool Session::IMEOn(commands::Command *command) {
command->mutable_output()->set_consumed(true);
ClearUndoContext();
SetSessionState(ImeContext::PRECOMPOSITION, context_.get());
if (command->input().has_key() && command->input().key().has_mode()) {
ApplyInputMode(
command->input().key().mode(), context_->mutable_composer());
}
OutputMode(command);
return true;
}
bool Session::IMEOff(commands::Command *command) {
command->mutable_output()->set_consumed(true);
ClearUndoContext();
Commit(command);
// Reset the context.
context_->mutable_converter()->Reset();
SetSessionState(ImeContext::DIRECT, context_.get());
OutputMode(command);
return true;
}
bool Session::MakeSureIMEOn(mozc::commands::Command *command) {
if (command->input().has_command() &&
command->input().command().has_composition_mode() &&
(command->input().command().composition_mode() == commands::DIRECT)) {
// This is invalid and unsupported usage.
return false;
}
command->mutable_output()->set_consumed(true);
if (context_->state() == ImeContext::DIRECT) {
ClearUndoContext();
SetSessionState(ImeContext::PRECOMPOSITION, context_.get());
}
if (command->input().has_command() &&
command->input().command().has_composition_mode()) {
ApplyInputMode(command->input().command().composition_mode(),
context_->mutable_composer());
}
OutputMode(command);
return true;
}
bool Session::MakeSureIMEOff(mozc::commands::Command *command) {
if (command->input().has_command() &&
command->input().command().has_composition_mode() &&
(command->input().command().composition_mode() == commands::DIRECT)) {
// This is invalid and unsupported usage.
return false;
}
command->mutable_output()->set_consumed(true);
if (context_->state() != ImeContext::DIRECT) {
ClearUndoContext();
Commit(command);
// Reset the context.
context_->mutable_converter()->Reset();
SetSessionState(ImeContext::DIRECT, context_.get());
}
if (command->input().has_command() &&
command->input().command().has_composition_mode()) {
ApplyInputMode(command->input().command().composition_mode(),
context_->mutable_composer());
}
OutputMode(command);
return true;
}
bool Session::EchoBack(commands::Command *command) {
command->mutable_output()->set_consumed(false);
context_->mutable_converter()->Reset();
OutputKey(command);
return true;
}
bool Session::EchoBackAndClearUndoContext(commands::Command *command) {
command->mutable_output()->set_consumed(false);
// Don't clear undo context when KeyEvent has a modifier key only.
// TODO(hsumita): A modifier key may be assigned to another functions.
// ex) InsertSpace
// We need to check it outside of this function.
const commands::KeyEvent &key_event = command->input().key();
if (!IsPureModifierKeyEvent(key_event)) {
ClearUndoContext();
}
return EchoBack(command);
}
bool Session::DoNothing(commands::Command *command) {
command->mutable_output()->set_consumed(true);
// Quick hack for zero query suggestion.
// Caveats: Resetting converter causes b/8703702 on Windows.
// Basically we should not *do* something in DoNothing.
// TODO(komatsu): Fix this.
if (context_->GetRequest().zero_query_suggestion() &&
context_->converter().IsActive() &&
(context_->state() == ImeContext::PRECOMPOSITION)) {
context_->mutable_converter()->Reset();
Output(command);
}
if (context_->state() & (ImeContext::COMPOSITION | ImeContext::CONVERSION)) {
Output(command);
}
return true;
}
bool Session::Revert(commands::Command *command) {
if (context_->state() == ImeContext::PRECOMPOSITION) {
context_->mutable_converter()->Revert();
return EchoBackAndClearUndoContext(command);
}
if (!(context_->state() & (ImeContext::COMPOSITION |
ImeContext::CONVERSION))) {
return DoNothing(command);
}
command->mutable_output()->set_consumed(true);
ClearUndoContext();
if (context_->state() == ImeContext::CONVERSION) {
context_->mutable_converter()->Cancel();
}
SetSessionState(ImeContext::PRECOMPOSITION, context_.get());
OutputMode(command);
return true;
}
bool Session::ResetContext(commands::Command *command) {
if (context_->state() == ImeContext::PRECOMPOSITION) {
context_->mutable_converter()->Reset();
return EchoBackAndClearUndoContext(command);
}
command->mutable_output()->set_consumed(true);
ClearUndoContext();
context_->mutable_converter()->Reset();
SetSessionState(ImeContext::PRECOMPOSITION, context_.get());
OutputMode(command);
return true;
}
void Session::SetTable(const composer::Table *table) {
ClearUndoContext();
context_.get()->mutable_composer()->SetTable(table);
}
void Session::ReloadConfig() {
UpdateConfig(config::ConfigHandler::GetConfig(), context_.get());
}
void Session::SetRequest(const commands::Request *request) {
ClearUndoContext();
context_->SetRequest(request);
}
// static
void Session::UpdateConfig(const config::Config &config, ImeContext *context) {
context->set_keymap(config.session_keymap());
Singleton<KeyEventTransformer>::get()->ReloadConfig(config);
context->mutable_composer()->ReloadConfig();
UpdateOperationPreferences(config, context);
}
// static
void Session::UpdateOperationPreferences(const config::Config &config,
ImeContext *context) {
OperationPreferences operation_preferences;
// Keyboard shortcut for candidates.
const char kShortcut123456789[] = "123456789";
const char kShortcutASDFGHJKL[] = "asdfghjkl";
config::Config::SelectionShortcut shortcut;
if (config.has_selection_shortcut()) {
shortcut = config.selection_shortcut();
} else {
shortcut = GET_CONFIG(selection_shortcut);
}
switch (shortcut) {
case config::Config::SHORTCUT_123456789:
operation_preferences.candidate_shortcuts = kShortcut123456789;
break;
case config::Config::SHORTCUT_ASDFGHJKL:
operation_preferences.candidate_shortcuts = kShortcutASDFGHJKL;
break;
case config::Config::NO_SHORTCUT:
operation_preferences.candidate_shortcuts.clear();
break;
default:
LOG(WARNING) << "Unkown shortcuts type: "
<< GET_CONFIG(selection_shortcut);
break;
}
// Cascading Window.
#ifndef OS_LINUX
if (config.has_use_cascading_window()) {
operation_preferences.use_cascading_window = config.use_cascading_window();
}
#endif
context->mutable_converter()->SetOperationPreferences(operation_preferences);
}
bool Session::GetStatus(commands::Command *command) {
OutputMode(command);
return true;
}
bool Session::RequestConvertReverse(commands::Command *command) {
if (context_->state() != ImeContext::PRECOMPOSITION &&
context_->state() != ImeContext::DIRECT) {
return DoNothing(command);
}
command->mutable_output()->set_consumed(true);
Output(command);
// Fill callback message.
commands::SessionCommand *session_command =
command->mutable_output()->mutable_callback()->mutable_session_command();
session_command->set_type(commands::SessionCommand::CONVERT_REVERSE);
return true;
}
bool Session::ConvertReverse(commands::Command *command) {
if (context_->state() != ImeContext::PRECOMPOSITION &&
context_->state() != ImeContext::DIRECT) {
return DoNothing(command);
}
const string &composition = command->input().command().text();
string reading;
if (!context_->mutable_converter()->GetReadingText(composition, &reading)) {
LOG(ERROR) << "Failed to get reading text";
return DoNothing(command);
}
composer::Composer *composer = context_->mutable_composer();
composer->Reset();
vector<string> reading_characters;
composer->InsertCharacterPreedit(reading);
composer->set_source_text(composition);
// start conversion here.
if (!context_->mutable_converter()->Convert(*composer)) {
LOG(ERROR) << "Failed to start conversion for reverse conversion";
return false;
}
command->mutable_output()->set_consumed(true);
SetSessionState(ImeContext::CONVERSION, context_.get());
context_->mutable_converter()->SetCandidateListVisible(true);
Output(command);
return true;
}
bool Session::RequestUndo(commands::Command *command) {
if (!(context_->state() & (ImeContext::PRECOMPOSITION |
ImeContext::CONVERSION |
ImeContext::COMPOSITION))) {
return DoNothing(command);
}
// If undo context is empty, echoes back the key event so that it can be
// handled by the application. b/5553298
if (context_->state() == ImeContext::PRECOMPOSITION &&
!prev_context_.get()) {
return EchoBack(command);
}
command->mutable_output()->set_consumed(true);
Output(command);
// Fill callback message.
commands::SessionCommand *session_command =
command->mutable_output()->mutable_callback()->mutable_session_command();
session_command->set_type(commands::SessionCommand::UNDO);
return true;
}
bool Session::Undo(commands::Command *command) {
if (!(context_->state() & (ImeContext::PRECOMPOSITION |
ImeContext::CONVERSION |
ImeContext::COMPOSITION))) {
return DoNothing(command);
}
command->mutable_output()->set_consumed(true);
// Check the undo context
if (!prev_context_.get()) {
return DoNothing(command);
}
// Rollback the last user history.
context_->mutable_converter()->Revert();
size_t result_size = 0;
if (context_->output().has_result()) {
// Check the client's capability
if (!(context_->client_capability().text_deletion() &
commands::Capability::DELETE_PRECEDING_TEXT)) {
return DoNothing(command);
}
result_size = Util::CharsLen(context_->output().result().value());
}
PopUndoContext();
if (result_size > 0) {
commands::DeletionRange *range =
command->mutable_output()->mutable_deletion_range();
range->set_offset(-static_cast<int>(result_size));
range->set_length(result_size);
}
Output(command);
return true;
}
bool Session::SelectCandidateInternal(commands::Command *command) {
// If the current state is not conversion, composition or
// precomposition, the candidate window should not be shown. (On
// composition or precomposition, the window is able to be shown as
// a suggestion window).
if (!(context_->state() & (ImeContext::CONVERSION |
ImeContext::COMPOSITION |
ImeContext::PRECOMPOSITION))) {
return false;
}
if (!command->input().has_command() ||
!command->input().command().has_id()) {
LOG(WARNING) << "input.command or input.command.id did not exist.";
return false;
}
if (!context_->converter().IsActive()) {
LOG(WARNING) << "converter is not active. (no candidates)";
return false;
}
command->mutable_output()->set_consumed(true);
context_->mutable_converter()->CandidateMoveToId(
command->input().command().id(), context_->composer());
SetSessionState(ImeContext::CONVERSION, context_.get());
return true;
}
bool Session::SelectCandidate(commands::Command *command) {
if (!SelectCandidateInternal(command)) {
return DoNothing(command);
}
Output(command);
return true;
}
bool Session::CommitCandidate(commands::Command *command) {
if (!(context_->state() & (ImeContext::COMPOSITION |
ImeContext::CONVERSION |
ImeContext::PRECOMPOSITION))) {
return false;
}
const commands::Input &input = command->input();
if (!input.has_command() ||
!input.command().has_id()) {
LOG(WARNING) << "input.command or input.command.id did not exist.";
return false;
}
if (!context_->converter().IsActive()) {
LOG(WARNING) << "converter is not active. (no candidates)";
return false;
}
command->mutable_output()->set_consumed(true);
PushUndoContext();
if (context_->state() & ImeContext::CONVERSION) {
// There is a focused candidate so just select a candidate based on
// input message and commit first segment.
context_->mutable_converter()->CandidateMoveToId(
input.command().id(), context_->composer());
CommitHeadToFocusedSegmentsInternal(command->input().context());
} else {
// No candidate is focused.
size_t consumed_key_size = 0;
if (context_->mutable_converter()->CommitSuggestionById(
input.command().id(), context_->composer(),
command->input().context(), &consumed_key_size)) {
if (consumed_key_size < context_->composer().GetLength()) {
// partial suggestion was committed.
context_->mutable_composer()->DeleteRange(0, consumed_key_size);
MoveCursorToEnd(command);
// Copy the previous output for Undo.
context_->mutable_output()->CopyFrom(command->output());
return true;
}
}
}
if (!context_->converter().IsActive()) {
// If the converter is not active (ie. the segment size was one.),
// the state should be switched to precomposition.
SetSessionState(ImeContext::PRECOMPOSITION, context_.get());
// Get suggestion if zero_query_suggestion is set.
// zero_query_suggestion is usually set where the client is a
// mobile.
if (context_->GetRequest().zero_query_suggestion()) {
Suggest(command->input());
}
}
Output(command);
// Copy the previous output for Undo.
context_->mutable_output()->CopyFrom(command->output());
return true;
}
bool Session::HighlightCandidate(commands::Command *command) {
if (!SelectCandidateInternal(command)) {
return false;
}
context_->mutable_converter()->SetCandidateListVisible(true);
Output(command);
return true;
}
bool Session::MaybeSelectCandidate(commands::Command *command) {
if (context_->state() != ImeContext::CONVERSION) {
return false;
}
// Note that SHORTCUT_ASDFGHJKL should be handled even when the CapsLock is
// enabled. This is why we need to normalize the key event here.
// See b/5655743.
commands::KeyEvent normalized_keyevent;
KeyEventUtil::NormalizeModifiers(command->input().key(),
&normalized_keyevent);
// Check if the input character is in the shortcut.
// TODO(komatsu): Support non ASCII characters such as Unicode and
// special keys.
const char shortcut = static_cast<char>(normalized_keyevent.key_code());
return context_->mutable_converter()->CandidateMoveToShortcut(shortcut);
}
void Session::set_client_capability(const commands::Capability &capability) {
context_->mutable_client_capability()->CopyFrom(capability);
}
void Session::set_application_info(const commands::ApplicationInfo
&application_info) {
context_->mutable_application_info()->CopyFrom(application_info);
}
const commands::ApplicationInfo &Session::application_info() const {
return context_->application_info();
}
uint64 Session::create_session_time() const {
return context_->create_time();
}
uint64 Session::last_command_time() const {
return context_->last_command_time();
}
bool Session::InsertCharacter(commands::Command *command) {
if (!command->input().has_key()) {
LOG(ERROR) << "No key event: " << command->input().DebugString();
return false;
}
const commands::KeyEvent &key = command->input().key();
if (key.input_style() == commands::KeyEvent::DIRECT_INPUT &&
context_->state() == ImeContext::PRECOMPOSITION) {
// If the key event represents a half width ascii character (ie.
// key_code is equal to key_string), that key event is not
// consumed and done echo back.
// We must not call |EchoBackAndClearUndoContext| for a half-width space
// here because it should be done in Session::TestSendKey or
// Session::InsertSpaceHalfWidth. Note that the |key| comes from
// Session::InsertSpaceHalfWidth and Session::InsertSpaceFullWidth is
// different from the original key event.
// For example, when the client sends a key command like
// {key.special_key(): HENKAN, key.modifier_keys(): [SHIFT]},
// Session::InsertSpaceHalfWidth replaces it with
// {key.key_string(): " ", key.key_code(): ' '}
// when you assign [Shift+HENKAN] to [InsertSpaceHalfWidth].
// So |key.key_code() == ' '| does not always mean that the original key is
// a space key w/o any modifier.
// This is why we cannot call |EchoBackAndClearUndoContext| when
// |key.key_code() == ' '|. This issue was found in b/5872031.
if (key.key_string().size() == 1 &&
key.key_code() == key.key_string()[0] &&
key.key_code() != ' ') {
return EchoBackAndClearUndoContext(command);
}
context_->mutable_composer()->InsertCharacterKeyEvent(key);
CommitCompositionDirectly(command);
ClearUndoContext(); // UndoContext must be invalidated.
return true;
}
command->mutable_output()->set_consumed(true);
// Handle shortcut keys selecting a candidate from a list.
if (MaybeSelectCandidate(command)) {
Output(command);
return true;
}
string composition;
context_->composer().GetQueryForConversion(&composition);
bool should_commit = (context_->state() == ImeContext::CONVERSION);
if (context_->GetRequest().space_on_alphanumeric() ==
commands::Request::SPACE_OR_CONVERT_COMMITING_COMPOSITION &&
context_->state() == ImeContext::COMPOSITION &&
// TODO(komatsu): Support FullWidthSpace
Util::EndsWith(composition, " ")) {
should_commit = true;
}
if (should_commit) {
CommitNotTriggeringZeroQuerySuggest(command);
if (key.input_style() == commands::KeyEvent::DIRECT_INPUT) {
// Do ClearUndoContext() because it is a direct input.
ClearUndoContext();
context_->mutable_composer()->InsertCharacterKeyEvent(key);
CommitCompositionDirectly(command);
return true;
}
}
context_->mutable_composer()->InsertCharacterKeyEvent(key);
if (context_->mutable_composer()->ShouldCommit()) {
CommitCompositionDirectly(command);
return true;
}
size_t length_to_commit = 0;
if (context_->composer().ShouldCommitHead(&length_to_commit)) {
return CommitHead(length_to_commit, command);
}
SetSessionState(ImeContext::COMPOSITION, context_.get());
if (CanStartAutoConversion(key)) {
return Convert(command);
}
if (Suggest(command->input())) {
Output(command);
return true;
}
OutputComposition(command);
return true;
}
bool Session::IsFullWidthInsertSpace(const commands::Input &input) const {
// If IME is off, any space has to be half-width.
if (context_->state() == ImeContext::DIRECT) {
return false;
}
// In this method, we should not update the actual input mode stored in
// the composer even when |input| has a new input mode. Note that this
// method can be called from TestSendKey, where internal input mode is
// is not expected to be changed. This is one of the reasons why this
// method is a const method.
// On the other hand, this method should behave as if the new input mode
// in |input| was applied. For example, this method should behave as if
// the current input mode was HALF_KATAKANA in the following situation.
// composer's input mode: HIRAGANA
// input.key().mode() : HALF_KATAKANA
// To achieve this, we create a temporary composer object to which the
// new input mode will be stored when |input| has a new input mode.
const composer::Composer* target_composer = &context_->composer();
scoped_ptr<composer::Composer> temporary_composer;
if (input.has_key() && input.key().has_mode()) {
// Allocate an object only when it is necessary.
temporary_composer.reset(new composer::Composer(NULL, NULL));
// Copy the current composer state just in case.
temporary_composer->CopyFrom(context_->composer());
ApplyInputMode(input.key().mode(), temporary_composer.get());
// Refer to this temporary composer in this method.
target_composer = temporary_composer.get();
}
// Check the current config and the current input status.
bool is_full_width = false;
switch (GET_CONFIG(space_character_form)) {
case config::Config::FUNDAMENTAL_INPUT_MODE: {
const transliteration::TransliterationType input_mode =
target_composer->GetInputMode();
if (transliteration::T13n::IsInHalfAsciiTypes(input_mode) ||
transliteration::T13n::IsInHalfKatakanaTypes(input_mode)) {
is_full_width = false;
} else {
is_full_width = true;
}
break;
}
case config::Config::FUNDAMENTAL_FULL_WIDTH:
is_full_width = true;
break;
case config::Config::FUNDAMENTAL_HALF_WIDTH:
is_full_width = false;
break;
default:
LOG(WARNING) << "Unknown input mode";
is_full_width = false;
break;
}
return is_full_width;
}
bool Session::InsertSpace(commands::Command *command) {
if (IsFullWidthInsertSpace(command->input())) {
return InsertSpaceFullWidth(command);
} else {
return InsertSpaceHalfWidth(command);
}
}
bool Session::InsertSpaceToggled(commands::Command *command) {
if (IsFullWidthInsertSpace(command->input())) {
return InsertSpaceHalfWidth(command);
} else {
return InsertSpaceFullWidth(command);
}
}
bool Session::InsertSpaceHalfWidth(commands::Command *command) {
if (!(context_->state() & (ImeContext::PRECOMPOSITION |
ImeContext::COMPOSITION |
ImeContext::CONVERSION))) {
return DoNothing(command);
}
if (context_->state() == ImeContext::PRECOMPOSITION) {
// TODO(komatsu): This is a hack to work around the problem with
// the inconsistency between TestSendKey and SendKey.
if (IsPureSpaceKey(command->input().key())) {
return EchoBackAndClearUndoContext(command);
}
// UndoContext will be cleared in |InsertCharacter| in this case.
}
const bool has_mode = command->input().key().has_mode();
const commands::CompositionMode mode = command->input().key().mode();
command->mutable_input()->clear_key();
commands::KeyEvent *key_event = command->mutable_input()->mutable_key();
key_event->set_key_code(' ');
key_event->set_key_string(" ");
key_event->set_input_style(commands::KeyEvent::DIRECT_INPUT);
if (has_mode) {
key_event->set_mode(mode);
}
return InsertCharacter(command);
}
bool Session::InsertSpaceFullWidth(commands::Command *command) {
if (!(context_->state() & (ImeContext::PRECOMPOSITION |
ImeContext::COMPOSITION |
ImeContext::CONVERSION))) {
return DoNothing(command);
}
if (context_->state() == ImeContext::PRECOMPOSITION) {
// UndoContext will be cleared in |InsertCharacter| in this case.
// TODO(komatsu): make sure if
// |context_->mutable_converter()->Reset()| is necessary here.
context_->mutable_converter()->Reset();
}
const bool has_mode = command->input().key().has_mode();
const commands::CompositionMode mode = command->input().key().mode();
command->mutable_input()->clear_key();
commands::KeyEvent *key_event = command->mutable_input()->mutable_key();
key_event->set_key_code(' ');
// " " (full-width space)
key_event->set_key_string("\xE3\x80\x80");
key_event->set_input_style(commands::KeyEvent::DIRECT_INPUT);
if (has_mode) {
key_event->set_mode(mode);
}
return InsertCharacter(command);
}
bool Session::TryCancelConvertReverse(commands::Command *command) {
// If source_text is set, it usually means this session started by a
// reverse conversion.
if (context_->composer().source_text().empty()) {
return false;
}
CommitSourceTextDirectly(command);
return true;
}
bool Session::EditCancelOnPasswordField(commands::Command *command) {
if (context_->composer().GetInputFieldType() != commands::Context::PASSWORD) {
return false;
}
// In password mode, we should commit preedit and close keyboard
// on Android.
// TODO(matsuzakit): Remove this trick. b/5955618
if (context_->composer().source_text().empty()) {
CommitCompositionDirectly(command);
} else {
// Commits original text of reverse conversion.
CommitSourceTextDirectly(command);
}
// Passes the key event through to MozcService.java
// to continue the processes which are invoked by cancel operation.
command->mutable_output()->set_consumed(false);
return true;
}
bool Session::EditCancel(commands::Command *command) {
if (EditCancelOnPasswordField(command)) {
return true;
}
command->mutable_output()->set_consumed(true);
// To work around b/5034698, we need to use OutputMode() unless the
// original text is restored to cancel reconversion.
const bool text_restored = TryCancelConvertReverse(command);
SetSessionState(ImeContext::PRECOMPOSITION, context_.get());
if (text_restored) {
Output(command);
} else {
// It is nice to use Output() instead of OutputMode(). However, if
// Output() is used, unnecessary candidate words are shown because
// the previous candidate state is not cleared here. To fix it, we
// should carefully modify SessionConverter. See b/5034698.
//
// TODO(komatsu): Use Output() instead of OutputMode.
OutputMode(command);
}
return true;
}
bool Session::EditCancelAndIMEOff(commands::Command *command) {
if (EditCancelOnPasswordField(command)) {
return true;
}
if (!(context_->state() & (ImeContext::PRECOMPOSITION |
ImeContext::COMPOSITION |
ImeContext::CONVERSION))) {
return DoNothing(command);
}
command->mutable_output()->set_consumed(true);
TryCancelConvertReverse(command);
ClearUndoContext();
// Reset the context.
context_->mutable_converter()->Reset();
SetSessionState(ImeContext::DIRECT, context_.get());
Output(command);
return true;
}
bool Session::CommitInternal(commands::Command *command,
bool trigger_zero_query_suggest) {
if (!(context_->state() & (ImeContext::COMPOSITION |
ImeContext::CONVERSION))) {
return DoNothing(command);
}
command->mutable_output()->set_consumed(true);
PushUndoContext();
if (context_->state() == ImeContext::COMPOSITION) {
context_->mutable_converter()->CommitPreedit(context_->composer(),
command->input().context());
} else { // ImeContext::CONVERSION
context_->mutable_converter()->Commit(context_->composer(),
command->input().context());
}
SetSessionState(ImeContext::PRECOMPOSITION, context_.get());
if (trigger_zero_query_suggest) {
Suggest(command->input());
}
Output(command);
// Copy the previous output for Undo.
context_->mutable_output()->CopyFrom(command->output());
return true;
}
bool Session::Commit(commands::Command *command) {
return CommitInternal(command,
context_->GetRequest().zero_query_suggestion());
}
bool Session::CommitNotTriggeringZeroQuerySuggest(commands::Command *command) {
return CommitInternal(command, false);
}
bool Session::CommitHead(size_t count, commands::Command *command) {
if (!(context_->state() &
(ImeContext::COMPOSITION | ImeContext::PRECOMPOSITION))) {
return DoNothing(command);
}
command->mutable_output()->set_consumed(true);
// TODO(yamaguchi): Support undo feature.
ClearUndoContext();
size_t committed_size;
context_->mutable_converter()->
CommitHead(count, context_->composer(), &committed_size);
context_->mutable_composer()->DeleteRange(0, committed_size);
Output(command);
return true;
}
bool Session::CommitFirstSuggestion(commands::Command *command) {
if (!(context_->state() == ImeContext::COMPOSITION ||
context_->state() == ImeContext::PRECOMPOSITION)) {
return DoNothing(command);
}
if (!context_->converter().IsActive()) {
return DoNothing(command);
}
command->mutable_output()->set_consumed(true);
PushUndoContext();
const int kFirstIndex = 0;
size_t committed_key_size = 0;
context_->mutable_converter()->CommitSuggestionByIndex(
kFirstIndex, context_->composer(), command->input().context(),
&committed_key_size);
SetSessionState(ImeContext::PRECOMPOSITION, context_.get());
// Get suggestion if zero_query_suggestion is set.
// zero_query_suggestion is usually set where the client is a mobile.
if (context_->GetRequest().zero_query_suggestion()) {
Suggest(command->input());
}
Output(command);
// Copy the previous output for Undo.
context_->mutable_output()->CopyFrom(command->output());
return true;
}
bool Session::CommitSegment(commands::Command *command) {
if (!(context_->state() & (ImeContext::CONVERSION))) {
return DoNothing(command);
}
command->mutable_output()->set_consumed(true);
PushUndoContext();
CommitFirstSegmentInternal(command->input().context());
if (!context_->converter().IsActive()) {
// If the converter is not active (ie. the segment size was one.),
// the state should be switched to precomposition.
SetSessionState(ImeContext::PRECOMPOSITION, context_.get());
// Get suggestion if zero_query_suggestion is set.
// zero_query_suggestion is usually set where the client is a mobile.
if (context_->GetRequest().zero_query_suggestion()) {
Suggest(command->input());
}
}
Output(command);
// Copy the previous output for Undo.
context_->mutable_output()->CopyFrom(command->output());
return true;
}
void Session::CommitFirstSegmentInternal(const commands::Context &context) {
size_t size;
context_->mutable_converter()->CommitFirstSegment(
context_->composer(), context, &size);
if (size > 0) {
// Delete the key characters of the first segment from the preedit.
context_->mutable_composer()->DeleteRange(0, size);
// The number of segments should be more than one.
DCHECK_GT(context_->composer().GetLength(), 0);
}
}
void Session::CommitHeadToFocusedSegmentsInternal(
const commands::Context &context) {
size_t size;
context_->mutable_converter()->CommitHeadToFocusedSegments(
context_->composer(), context, &size);
if (size > 0) {
// Delete the key characters of the first segment from the preedit.
context_->mutable_composer()->DeleteRange(0, size);
// The number of segments should be more than one.
DCHECK_GT(context_->composer().GetLength(), 0);
}
}
void Session::CommitCompositionDirectly(commands::Command *command) {
string composition, conversion;
context_->composer().GetQueryForConversion(&composition);
context_->composer().GetStringForSubmission(&conversion);
CommitStringDirectly(composition, conversion, command);
}
void Session::CommitSourceTextDirectly(commands::Command *command) {
// We cannot use a reference since composer will be cleared on
// CommitStringDirectly.
const string copied_source_text = context_->composer().source_text();
CommitStringDirectly(copied_source_text, copied_source_text, command);
}
void Session::CommitRawTextDirectly(commands::Command *command) {
string raw_text;
context_->composer().GetRawString(&raw_text);
CommitStringDirectly(raw_text, raw_text, command);
}
void Session::CommitStringDirectly(const string &key, const string &preedit,
commands::Command *command) {
if (key.empty() || preedit.empty()) {
return;
}
command->mutable_output()->set_consumed(true);
context_->mutable_converter()->Reset();
commands::Result *result = command->mutable_output()->mutable_result();
DCHECK(result != NULL);
result->set_type(commands::Result::STRING);
result->mutable_key()->append(key);
result->mutable_value()->append(preedit);
SetSessionState(ImeContext::PRECOMPOSITION, context_.get());
// Get suggestion if zero_query_suggestion is set.
// zero_query_suggestion is usually set where the client is a mobile.
if (context_->GetRequest().zero_query_suggestion()) {
Suggest(command->input());
}
Output(command);
}
namespace {
bool SuppressSuggestion(const commands::Input &input) {
if (!input.has_context()) {
return false;
}
// If the target input field is in Chrome's Omnibox or Google
// search box, the suggest window is hidden.
for (size_t i = 0; i < input.context().experimental_features_size(); ++i) {
const string &feature = input.context().experimental_features(i);
if (feature == "chrome_omnibox" || feature == "google_search_box") {
return true;
}
}
return false;
}
} // namespace
bool Session::Suggest(const commands::Input &input) {
if (SuppressSuggestion(input)) {
return false;
}
// |reuqest_suggestion| is not supposed to always ensure suppressing
// suggestion since this field is used for performance improvement
// by skipping interim suggestions. However, the implementation of
// SessionConverter::SuggestWithPreferences does not perform suggest
// whenever this flag is on. So the caller should consider whether
// this flag should be set or not. Because the original logic was
// implemented in Session::InserCharacter, we check the input.type()
// is SEND_KEY assuming SEND_KEY results InsertCharacter (in most
// cases).
//
// TODO(komatsu): Move the logic into SessionConverter.
if (input.has_request_suggestion() &&
input.type() == commands::Input::SEND_KEY) {
ConversionPreferences conversion_preferences =
context_->converter().conversion_preferences();
conversion_preferences.request_suggestion = input.request_suggestion();
return context_->mutable_converter()->SuggestWithPreferences(
context_->composer(), conversion_preferences);
}
return context_->mutable_converter()->Suggest(context_->composer());
}
bool Session::ConvertToTransliteration(
commands::Command *command,
const transliteration::TransliterationType type) {
if (!(context_->state() & (ImeContext::CONVERSION |
ImeContext::COMPOSITION))) {
return DoNothing(command);
}
command->mutable_output()->set_consumed(true);
if (!context_->mutable_converter()->ConvertToTransliteration(
context_->composer(), type)) {
return false;
}
SetSessionState(ImeContext::CONVERSION, context_.get());
Output(command);
return true;
}
bool Session::ConvertToHiragana(commands::Command *command) {
return ConvertToTransliteration(command, transliteration::HIRAGANA);
}
bool Session::ConvertToFullKatakana(commands::Command *command) {
return ConvertToTransliteration(command, transliteration::FULL_KATAKANA);
}
bool Session::ConvertToHalfKatakana(commands::Command *command) {
return ConvertToTransliteration(command, transliteration::HALF_KATAKANA);
}
bool Session::ConvertToFullASCII(commands::Command *command) {
return ConvertToTransliteration(command, transliteration::FULL_ASCII);
}
bool Session::ConvertToHalfASCII(commands::Command *command) {
return ConvertToTransliteration(command, transliteration::HALF_ASCII);
}
bool Session::SwitchKanaType(commands::Command *command) {
if (!(context_->state() & (ImeContext::CONVERSION |
ImeContext::COMPOSITION))) {
return DoNothing(command);
}
command->mutable_output()->set_consumed(true);
if (!context_->mutable_converter()->SwitchKanaType(context_->composer())) {
return false;
}
SetSessionState(ImeContext::CONVERSION, context_.get());
Output(command);
return true;
}
bool Session::DisplayAsHiragana(commands::Command *command) {
command->mutable_output()->set_consumed(true);
if (context_->state() == ImeContext::CONVERSION) {
return ConvertToHiragana(command);
} else { // context_->state() == ImeContext::COMPOSITION
context_->mutable_composer()->SetOutputMode(transliteration::HIRAGANA);
OutputComposition(command);
return true;
}
}
bool Session::DisplayAsFullKatakana(commands::Command *command) {
command->mutable_output()->set_consumed(true);
if (context_->state() == ImeContext::CONVERSION) {
return ConvertToFullKatakana(command);
} else { // context_->state() == ImeContext::COMPOSITION
context_->mutable_composer()->SetOutputMode(transliteration::FULL_KATAKANA);
OutputComposition(command);
return true;
}
}
bool Session::DisplayAsHalfKatakana(commands::Command *command) {
command->mutable_output()->set_consumed(true);
if (context_->state() == ImeContext::CONVERSION) {
return ConvertToHalfKatakana(command);
} else { // context_->state() == ImeContext::COMPOSITION
context_->mutable_composer()->SetOutputMode(transliteration::HALF_KATAKANA);
OutputComposition(command);
return true;
}
}
bool Session::TranslateFullASCII(commands::Command *command) {
command->mutable_output()->set_consumed(true);
if (context_->state() == ImeContext::CONVERSION) {
return ConvertToFullASCII(command);
} else { // context_->state() == ImeContext::COMPOSITION
context_->mutable_composer()->SetOutputMode(
transliteration::T13n::ToggleFullAsciiTypes(
context_->composer().GetOutputMode()));
OutputComposition(command);
return true;
}
}
bool Session::TranslateHalfASCII(commands::Command *command) {
command->mutable_output()->set_consumed(true);
if (context_->state() == ImeContext::CONVERSION) {
return ConvertToHalfASCII(command);
} else { // context_->state() == ImeContext::COMPOSITION
context_->mutable_composer()->SetOutputMode(
transliteration::T13n::ToggleHalfAsciiTypes(
context_->composer().GetOutputMode()));
OutputComposition(command);
return true;
}
}
bool Session::InputModeHiragana(commands::Command *command) {
command->mutable_output()->set_consumed(true);
EnsureIMEIsOn();
// The temporary mode should not be overridden.
SwitchInputMode(transliteration::HIRAGANA, context_->mutable_composer());
OutputFromState(command);
return true;
}
bool Session::InputModeFullKatakana(commands::Command *command) {
command->mutable_output()->set_consumed(true);
EnsureIMEIsOn();
// The temporary mode should not be overridden.
SwitchInputMode(transliteration::FULL_KATAKANA, context_->mutable_composer());
OutputFromState(command);
return true;
}
bool Session::InputModeHalfKatakana(commands::Command *command) {
command->mutable_output()->set_consumed(true);
EnsureIMEIsOn();
// The temporary mode should not be overridden.
SwitchInputMode(transliteration::HALF_KATAKANA, context_->mutable_composer());
OutputFromState(command);
return true;
}
bool Session::InputModeFullASCII(commands::Command *command) {
command->mutable_output()->set_consumed(true);
EnsureIMEIsOn();
// The temporary mode should not be overridden.
SwitchInputMode(transliteration::FULL_ASCII, context_->mutable_composer());
OutputFromState(command);
return true;
}
bool Session::InputModeHalfASCII(commands::Command *command) {
command->mutable_output()->set_consumed(true);
EnsureIMEIsOn();
// The temporary mode should not be overridden.
SwitchInputMode(transliteration::HALF_ASCII, context_->mutable_composer());
OutputFromState(command);
return true;
}
bool Session::InputModeSwitchKanaType(commands::Command *command) {
if (context_->state() != ImeContext::PRECOMPOSITION) {
return DoNothing(command);
}
command->mutable_output()->set_consumed(true);
transliteration::TransliterationType current_type =
context_->composer().GetInputMode();
transliteration::TransliterationType next_type;
switch (current_type) {
case transliteration::HIRAGANA:
next_type = transliteration::FULL_KATAKANA;
break;
case transliteration::FULL_KATAKANA:
next_type = transliteration::HALF_KATAKANA;
break;
case transliteration::HALF_KATAKANA:
next_type = transliteration::HIRAGANA;
break;
case transliteration::HALF_ASCII:
case transliteration::FULL_ASCII:
next_type = current_type;
break;
default:
LOG(ERROR) << "Unknown input mode: " << current_type;
// don't change input mode
next_type = current_type;
break;
}
// The temporary mode should not be overridden.
SwitchInputMode(next_type, context_->mutable_composer());
OutputFromState(command);
return true;
}
bool Session::ConvertToHalfWidth(commands::Command *command) {
if (!(context_->state() & (ImeContext::CONVERSION |
ImeContext::COMPOSITION))) {
return DoNothing(command);
}
command->mutable_output()->set_consumed(true);
if (!context_->mutable_converter()->ConvertToHalfWidth(
context_->composer())) {
return false;
}
SetSessionState(ImeContext::CONVERSION, context_.get());
Output(command);
return true;
}
bool Session::TranslateHalfWidth(commands::Command *command) {
command->mutable_output()->set_consumed(true);
if (context_->state() == ImeContext::CONVERSION) {
return ConvertToHalfWidth(command);
} else { // context_->state() == ImeContext::COMPOSITION
const transliteration::TransliterationType type =
context_->composer().GetOutputMode();
if (type == transliteration::HIRAGANA ||
type == transliteration::FULL_KATAKANA ||
type == transliteration::HALF_KATAKANA) {
context_->mutable_composer()->SetOutputMode(
transliteration::HALF_KATAKANA);
} else if (type == transliteration::FULL_ASCII) {
context_->mutable_composer()->SetOutputMode(transliteration::HALF_ASCII);
} else if (type == transliteration::FULL_ASCII_UPPER) {
context_->mutable_composer()->SetOutputMode(
transliteration::HALF_ASCII_UPPER);
} else if (type == transliteration::FULL_ASCII_LOWER) {
context_->mutable_composer()->SetOutputMode(
transliteration::HALF_ASCII_LOWER);
} else if (type == transliteration::FULL_ASCII_CAPITALIZED) {
context_->mutable_composer()->SetOutputMode(
transliteration::HALF_ASCII_CAPITALIZED);
} else {
// transliteration::HALF_ASCII_something
return TranslateHalfASCII(command);
}
OutputComposition(command);
return true;
}
}
bool Session::LaunchConfigDialog(commands::Command *command) {
command->mutable_output()->set_launch_tool_mode(
commands::Output::CONFIG_DIALOG);
return DoNothing(command);
}
bool Session::LaunchDictionaryTool(commands::Command *command) {
command->mutable_output()->set_launch_tool_mode(
commands::Output::DICTIONARY_TOOL);
return DoNothing(command);
}
bool Session::LaunchWordRegisterDialog(commands::Command *command) {
command->mutable_output()->set_launch_tool_mode(
commands::Output::WORD_REGISTER_DIALOG);
return DoNothing(command);
}
bool Session::UndoOrRewind(commands::Command *command) {
// Rewind if the state is in composition.
if (context_->state() & ImeContext::COMPOSITION) {
command->mutable_output()->set_consumed(true);
return SendComposerCommand(composer::Composer::REWIND, command);
}
// Undo if we can order UNDO command.
if (prev_context_.get()) {
return Undo(command);
}
return DoNothing(command);
}
bool Session::SendComposerCommand(
const composer::Composer::InternalCommand composer_command,
commands::Command *command) {
if (!(context_->state() & ImeContext::COMPOSITION)) {
DLOG(WARNING) << "State : " << context_->state();
return false;
}
context_->mutable_composer()->InsertCommandCharacter(composer_command);
// InsertCommandCharacter method updates the preedit text
// so we need to update suggest candidates.
if (Suggest(command->input())) {
Output(command);
return true;
}
OutputComposition(command);
return true;
}
bool Session::ToggleAlphanumericMode(commands::Command *command) {
command->mutable_output()->set_consumed(true);
context_->mutable_composer()->ToggleInputMode();
OutputFromState(command);
return true;
}
bool Session::DeleteSelectedCandidateFromHistory(commands::Command *command) {
const Segment::Candidate *cand =
context_->converter().GetSelectedCandidateOfFocusedSegment();
if (cand == NULL) {
LOG(WARNING) << "No candidate is selected.";
return DoNothing(command);
}
UserDataManagerInterface *manager = engine_->GetUserDataManager();
if (!manager->ClearUserPredictionEntry(cand->key, cand->value)) {
DLOG(WARNING) << "Cannot delete non-history candidate or deletion failed: "
<< cand->DebugString();
return DoNothing(command);
}
return ConvertCancel(command);
}
bool Session::Convert(commands::Command *command) {
command->mutable_output()->set_consumed(true);
string composition;
context_->composer().GetQueryForConversion(&composition);
// TODO(komatsu): Make a function like ConvertOrSpace.
// Handle a space key on the ASCII composition mode.
if (context_->state() == ImeContext::COMPOSITION &&
(context_->composer().GetInputMode() == transliteration::HALF_ASCII ||
context_->composer().GetInputMode() == transliteration::FULL_ASCII) &&
command->input().key().has_special_key() &&
command->input().key().special_key() == commands::KeyEvent::SPACE) {
// TODO(komatsu): Consider FullWidth Space too.
if (!Util::EndsWith(composition, " ")) {
if (context_->GetRequest().space_on_alphanumeric() ==
commands::Request::COMMIT) {
// Space is committed with the composition
context_->mutable_composer()->InsertCharacterPreedit(" ");
return Commit(command);
} else {
// SPACE_OR_CONVERT_KEEPING_COMPOSITION or
// SPACE_OR_CONVERT_COMMITING_COMPOSITION.
// If the last character is not space, space is inserted to the
// composition.
command->mutable_input()->mutable_key()->set_key_code(' ');
return InsertCharacter(command);
}
}
if (!composition.empty()) {
DCHECK_EQ(' ', composition[composition.size() - 1]);
// Delete the last space.
context_->mutable_composer()->Backspace();
}
}
if (!context_->mutable_converter()->Convert(context_->composer())) {
LOG(ERROR) << "Conversion failed for some reasons.";
OutputComposition(command);
return true;
}
SetSessionState(ImeContext::CONVERSION, context_.get());
Output(command);
return true;
}
bool Session::ConvertWithoutHistory(commands::Command *command) {
command->mutable_output()->set_consumed(true);
ConversionPreferences preferences =
context_->converter().conversion_preferences();
preferences.use_history = false;
if (!context_->mutable_converter()->ConvertWithPreferences(
context_->composer(), preferences)) {
LOG(ERROR) << "Conversion failed for some reasons.";
OutputComposition(command);
return true;
}
SetSessionState(ImeContext::CONVERSION, context_.get());
Output(command);
return true;
}
bool Session::CommitIfPassword(commands::Command *command) {
if (context_->composer().GetInputFieldType() == commands::Context::PASSWORD) {
CommitCompositionDirectly(command);
return true;
}
return false;
}
bool Session::MoveCursorRight(commands::Command *command) {
// In future, we may want to change the strategy of committing, to support
// more flexible behavior.
// - If the composing text has some "pending toggling character(s) at the
// end", we'd like to "fix" the toggling state, but not to commit.
// - Otherwise (i.e. if there is no such character(s)), we'd like to commit
// (considering the use cases, probably we'd like to apply it only for
// alphabet mode).
// Before supporting it, we'll need to support auto fixing by waiting
// a period. Also, it is necessary to support displaying the current toggling
// state (otherwise, users would be confused).
// So, to keep users out from such confusion, we only commit if the current
// composing mode doesn't has toggling state. Clients has the responsibility
// to check if the keyboard has toggling state or not. Note that the server
// should know the current table has toggling state or not. However,
// a client may NOT want to auto committing even if the composition mode
// doesn't have the toggling state, so the server just relies on the flag
// passed from the client.
// TODO(hidehiko): Support it, when it is prioritized.
if (context_->GetRequest().crossing_edge_behavior() ==
commands::Request::COMMIT_WITHOUT_CONSUMING &&
context_->composer().GetLength() == context_->composer().GetCursor()) {
Commit(command);
// Do not consume.
command->mutable_output()->set_consumed(false);
return true;
}
command->mutable_output()->set_consumed(true);
if (CommitIfPassword(command)) {
return true;
}
context_->mutable_composer()->MoveCursorRight();
if (Suggest(command->input())) {
Output(command);
return true;
}
OutputComposition(command);
return true;
}
bool Session::MoveCursorLeft(commands::Command *command) {
if (context_->GetRequest().crossing_edge_behavior() ==
commands::Request::COMMIT_WITHOUT_CONSUMING &&
context_->composer().GetCursor() == 0) {
Commit(command);
// Move the cursor to the beginning of the values.
command->mutable_output()->mutable_result()->set_cursor_offset(
-static_cast<int32>(
Util::CharsLen(command->output().result().value())));
// Do not consume.
command->mutable_output()->set_consumed(false);
return true;
}
command->mutable_output()->set_consumed(true);
if (CommitIfPassword(command)) {
return true;
}
context_->mutable_composer()->MoveCursorLeft();
if (Suggest(command->input())) {
Output(command);
return true;
}
OutputComposition(command);
return true;
}
bool Session::MoveCursorToEnd(commands::Command *command) {
command->mutable_output()->set_consumed(true);
if (CommitIfPassword(command)) {
return true;
}
context_->mutable_composer()->MoveCursorToEnd();
if (Suggest(command->input())) {
Output(command);
return true;
}
OutputComposition(command);
return true;
}
bool Session::MoveCursorTo(commands::Command *command) {
if (context_->state() != ImeContext::COMPOSITION) {
return DoNothing(command);
}
command->mutable_output()->set_consumed(true);
if (CommitIfPassword(command)) {
return true;
}
context_->mutable_composer()->
MoveCursorTo(command->input().command().cursor_position());
if (Suggest(command->input())) {
Output(command);
return true;
}
OutputComposition(command);
return true;
}
bool Session::MoveCursorToBeginning(commands::Command *command) {
command->mutable_output()->set_consumed(true);
if (CommitIfPassword(command)) {
return true;
}
context_->mutable_composer()->MoveCursorToBeginning();
if (Suggest(command->input())) {
Output(command);
return true;
}
OutputComposition(command);
return true;
}
bool Session::Delete(commands::Command *command) {
command->mutable_output()->set_consumed(true);
context_->mutable_composer()->Delete();
if (context_->mutable_composer()->Empty()) {
SetSessionState(ImeContext::PRECOMPOSITION, context_.get());
OutputMode(command);
} else if (Suggest(command->input())) {
Output(command);
return true;
} else {
OutputComposition(command);
}
return true;
}
bool Session::Backspace(commands::Command *command) {
command->mutable_output()->set_consumed(true);
context_->mutable_composer()->Backspace();
if (context_->mutable_composer()->Empty()) {
SetSessionState(ImeContext::PRECOMPOSITION, context_.get());
OutputMode(command);
} else if (Suggest(command->input())) {
Output(command);
return true;
} else {
OutputComposition(command);
}
return true;
}
bool Session::SegmentFocusRight(commands::Command *command) {
if (!(context_->state() & (ImeContext::CONVERSION))) {
return DoNothing(command);
}
command->mutable_output()->set_consumed(true);
context_->mutable_converter()->SegmentFocusRight();
Output(command);
return true;
}
bool Session::SegmentFocusLast(commands::Command *command) {
if (!(context_->state() & (ImeContext::CONVERSION))) {
return DoNothing(command);
}
command->mutable_output()->set_consumed(true);
context_->mutable_converter()->SegmentFocusLast();
Output(command);
return true;
}
bool Session::SegmentFocusLeft(commands::Command *command) {
if (!(context_->state() & (ImeContext::CONVERSION))) {
return DoNothing(command);
}
command->mutable_output()->set_consumed(true);
context_->mutable_converter()->SegmentFocusLeft();
Output(command);
return true;
}
bool Session::SegmentFocusLeftEdge(commands::Command *command) {
if (!(context_->state() & (ImeContext::CONVERSION))) {
return DoNothing(command);
}
command->mutable_output()->set_consumed(true);
context_->mutable_converter()->SegmentFocusLeftEdge();
Output(command);
return true;
}
bool Session::SegmentWidthExpand(commands::Command *command) {
if (!(context_->state() & (ImeContext::CONVERSION))) {
return DoNothing(command);
}
command->mutable_output()->set_consumed(true);
context_->mutable_converter()->SegmentWidthExpand(context_->composer());
Output(command);
return true;
}
bool Session::SegmentWidthShrink(commands::Command *command) {
if (!(context_->state() & (ImeContext::CONVERSION))) {
return DoNothing(command);
}
command->mutable_output()->set_consumed(true);
context_->mutable_converter()->SegmentWidthShrink(context_->composer());
Output(command);
return true;
}
bool Session::ReportBug(commands::Command *command) {
return DoNothing(command);
}
bool Session::ConvertNext(commands::Command *command) {
command->mutable_output()->set_consumed(true);
context_->mutable_converter()->CandidateNext(context_->composer());
Output(command);
return true;
}
bool Session::ConvertNextPage(commands::Command *command) {
if (!(context_->state() & (ImeContext::CONVERSION))) {
return DoNothing(command);
}
command->mutable_output()->set_consumed(true);
context_->mutable_converter()->CandidateNextPage();
Output(command);
return true;
}
bool Session::ConvertPrev(commands::Command *command) {
command->mutable_output()->set_consumed(true);
context_->mutable_converter()->CandidatePrev();
Output(command);
return true;
}
bool Session::ConvertPrevPage(commands::Command *command) {
if (!(context_->state() & (ImeContext::CONVERSION))) {
return DoNothing(command);
}
command->mutable_output()->set_consumed(true);
context_->mutable_converter()->CandidatePrevPage();
Output(command);
return true;
}
bool Session::ConvertCancel(commands::Command *command) {
command->mutable_output()->set_consumed(true);
SetSessionState(ImeContext::COMPOSITION, context_.get());
context_->mutable_converter()->Cancel();
if (Suggest(command->input())) {
Output(command);
} else {
OutputComposition(command);
}
return true;
}
bool Session::PredictAndConvert(commands::Command *command) {
if (context_->state() == ImeContext::CONVERSION) {
return ConvertNext(command);
}
command->mutable_output()->set_consumed(true);
if (context_->mutable_converter()->Predict(context_->composer())) {
SetSessionState(ImeContext::CONVERSION, context_.get());
Output(command);
} else {
OutputComposition(command);
}
return true;
}
bool Session::ExpandSuggestion(commands::Command *command) {
if (context_->state() == ImeContext::CONVERSION ||
context_->state() == ImeContext::DIRECT) {
return DoNothing(command);
}
command->mutable_output()->set_consumed(true);
context_->mutable_converter()->ExpandSuggestion(context_->composer());
Output(command);
return true;
}
void Session::OutputFromState(commands::Command *command) {
if (context_->state() == ImeContext::PRECOMPOSITION) {
OutputMode(command);
return;
}
if (context_->state() == ImeContext::COMPOSITION) {
OutputComposition(command);
return;
}
if (context_->state() == ImeContext::CONVERSION) {
Output(command);
return;
}
OutputMode(command);
}
void Session::Output(commands::Command *command) {
OutputMode(command);
context_->mutable_converter()->PopOutput(
context_->composer(), command->mutable_output());
OutputWindowLocation(command);
}
void Session::OutputWindowLocation(commands::Command *command) const {
if (!(command->output().has_candidates() &&
context_->caret_rectangle().IsInitialized() &&
context_->composition_rectangle().IsInitialized())) {
return;
}
DCHECK(command->output().candidates().has_category());
commands::Candidates *candidates =
command->mutable_output()->mutable_candidates();
candidates->mutable_caret_rectangle()->CopyFrom(
context_->caret_rectangle());
candidates->mutable_composition_rectangle()->CopyFrom(
context_->composition_rectangle());
if (command->output().candidates().category() == commands::SUGGESTION ||
command->output().candidates().category() == commands::PREDICTION) {
candidates->set_window_location(commands::Candidates::COMPOSITION);
} else {
candidates->set_window_location(commands::Candidates::CARET);
}
}
void Session::OutputMode(commands::Command *command) const {
const commands::CompositionMode mode =
ToCompositionMode(context_->composer().GetInputMode());
const commands::CompositionMode comeback_mode =
ToCompositionMode(context_->composer().GetComebackInputMode());
commands::Output *output = command->mutable_output();
commands::Status *status = output->mutable_status();
if (context_->state() == ImeContext::DIRECT) {
output->set_mode(commands::DIRECT);
status->set_activated(false);
} else {
output->set_mode(mode);
status->set_activated(true);
}
status->set_mode(mode);
status->set_comeback_mode(comeback_mode);
}
void Session::OutputComposition(commands::Command *command) const {
OutputMode(command);
commands::Preedit *preedit = command->mutable_output()->mutable_preedit();
SessionOutput::FillPreedit(context_->composer(), preedit);
}
void Session::OutputKey(commands::Command *command) const {
OutputMode(command);
commands::KeyEvent *key = command->mutable_output()->mutable_key();
key->CopyFrom(command->input().key());
}
namespace {
// return
// ((key_code == static_cast<uint32>('.') ||
// key_string == "." || key_string == "." ||
// key_string == "。" || key_string == "。") &&
// (config.auto_conversion_key() &
// config::Config::AUTO_CONVERSION_KUTEN)) ||
// ((key_code == static_cast<uint32>(',') ||
// key_string == "," || key_string == "," ||
// key_string == "、" || key_string == "、") &&
// (config.auto_conversion_key() &
// config::Config::AUTO_CONVERSION_TOUTEN)) ||
// ((key_code == static_cast<uint32>('?') ||
// key_string == "?" || key_string == "?") &&
// (config.auto_conversion_key() &
// config::Config::AUTO_CONVERSION_QUESTION_MARK)) ||
// ((key_code == static_cast<uint32>('!') ||
// key_string == "!" || key_string == "!") &&
// (config.auto_conversion_key() &
// config::Config::AUTO_CONVERSION_EXCLAMATION_MARK));
bool IsValidKey(const config::Config &config,
const uint32 key_code, const string &key_string) {
return
(((key_code == static_cast<uint32>('.') && key_string.empty()) ||
key_string == "." || key_string == "\xEF\xBC\x8E" ||
key_string == "\xE3\x80\x82" || key_string == "\xEF\xBD\xA1") &&
(config.auto_conversion_key() &
config::Config::AUTO_CONVERSION_KUTEN)) ||
(((key_code == static_cast<uint32>(',') && key_string.empty()) ||
key_string == "," || key_string == "\xEF\xBC\x8C" ||
key_string == "\xE3\x80\x81" || key_string == "\xEF\xBD\xA4") &&
(config.auto_conversion_key() &
config::Config::AUTO_CONVERSION_TOUTEN)) ||
(((key_code == static_cast<uint32>('?') && key_string.empty()) ||
key_string == "?" || key_string == "\xEF\xBC\x9F") &&
(config.auto_conversion_key() &
config::Config::AUTO_CONVERSION_QUESTION_MARK)) ||
(((key_code == static_cast<uint32>('!') && key_string.empty()) ||
key_string == "!" || key_string == "\xEF\xBC\x81") &&
(config.auto_conversion_key() &
config::Config::AUTO_CONVERSION_EXCLAMATION_MARK));
}
} // namespace
bool Session::CanStartAutoConversion(
const commands::KeyEvent &key_event) const {
if (!GET_CONFIG(use_auto_conversion)) {
return false;
}
// Disable if the input comes from non-standard user keyboards, like numpad.
// http://b/issue?id=2932067
if (key_event.input_style() != commands::KeyEvent::FOLLOW_MODE) {
return false;
}
// This is a tentative workaround for the bug http://b/issue?id=2932028
// When user types <Shift Down>O<Shift Up>racle<Shift Down>!<Shift Up>,
// The final "!" must be half-width, however, due to the limitation
// of converter interface, we don't have a good way to change it halfwidth, as
// the default preference of "!" is fullwidth. Basically, the converter is
// not composition-mode-aware.
// We simply disable the auto conversion feature if the mode is ASCII.
// We conclude that disabling this feature is better in this situation.
// TODO(taku): fix the behavior. Converter module needs to be fixed.
if (key_event.mode() == commands::HALF_ASCII ||
key_event.mode() == commands::FULL_ASCII) {
return false;
}
// We should NOT check key_string. http://b/issue?id=3217992
// Auto conversion is not triggered if the composition is empty or
// only one character, or the cursor is not in the end of the
// composition.
const size_t length = context_->composer().GetLength();
if (length <= 1 || length != context_->composer().GetCursor()) {
return false;
}
const config::Config &config = config::ConfigHandler::GetConfig();
const uint32 key_code = key_event.key_code();
string preedit;
context_->composer().GetStringForPreedit(&preedit);
const string last_char = Util::SubString(preedit, length - 1, 1);
if (last_char.empty()) {
return false;
}
// Check last character as user may change romaji table,
// For instance, if user assigns "." as "foo", we don't
// want to invoke auto_conversion.
if (!IsValidKey(config, key_code, last_char)) {
return false;
}
// check the previous character of last_character.
// when |last_prev_char| is number, we don't invoke auto_conversion
// if the same invoke key is repeated, do not conversion.
// http://b/issue?id=2932118
const string last_prev_char = Util::SubString(preedit, length - 2, 1);
if (last_prev_char.empty() || last_prev_char == last_char ||
Util::NUMBER == Util::GetScriptType(last_prev_char)) {
return false;
}
return true;
}
void Session::UpdateTime() {
context_->set_last_command_time(Util::GetTime());
}
void Session::TransformInput(commands::Input *input) {
if (input->has_key()) {
Singleton<KeyEventTransformer>::get()->TransformKeyEvent(
input->mutable_key());
}
}
bool Session::SwitchInputFieldType(commands::Command *command) {
command->mutable_output()->set_consumed(true);
context_->mutable_composer()->SetInputFieldType(
command->input().context().input_field_type());
Output(command);
return true;
}
bool Session::SetCaretLocation(commands::Command *command) {
if (!command->input().has_command()) {
return false;
}
const commands::SessionCommand &session_command = command->input().command();
if (!session_command.has_caret_rectangle()) {
context_->mutable_caret_rectangle()->Clear();
return false;
}
if (!context_->caret_rectangle().IsInitialized()) {
context_->mutable_caret_rectangle()->CopyFrom(
session_command.caret_rectangle());
return true;
}
const int caret_delta_y = abs(
context_->caret_rectangle().y() - session_command.caret_rectangle().y());
context_->mutable_caret_rectangle()->CopyFrom(
session_command.caret_rectangle());
const int kJumpThreshold = 30;
// If caret is jumped, assume the text field is also jumped and reset the
// rectangle of composition text.
if (caret_delta_y > kJumpThreshold) {
context_->mutable_composition_rectangle()->CopyFrom(
context_->caret_rectangle());
}
return true;
}
bool Session::HandleIndirectImeOnOff(commands::Command *command) {
const commands::KeyEvent &key = command->input().key();
if (!key.has_activated()) {
return true;
}
const ImeContext::State state = context_->state();
if (state == ImeContext::DIRECT && key.activated()) {
// Indirect IME On found.
commands::Command on_command;
on_command.CopyFrom(*command);
if (!IMEOn(&on_command)) {
return false;
}
} else if (state != ImeContext::DIRECT && !key.activated()) {
// Indirect IME Off found.
commands::Command off_command;
off_command.CopyFrom(*command);
if (!IMEOff(&off_command)) {
return false;
}
}
return true;
}
bool Session::CommitRawText(commands::Command *command) {
if (context_->composer().GetLength() == 0) {
return false;
}
CommitRawTextDirectly(command);
return true;
}
// TODO(komatsu): delete this function.
composer::Composer *Session::get_internal_composer_only_for_unittest() {
return context_->mutable_composer();
}
const ImeContext &Session::context() const {
return *context_;
}
} // namespace session
} // namespace mozc