blob: dfb94a5283c17e699eb281240db86df40170e775 [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.
/**
* @fileoverview This file contains NaclMozc class implementation.
*
* NaclMozc class provides Japanese input method for Chrome OS. The main logic
* of Japanese conversion is written in C++ and is executed in NaCl environment.
* This class communicate with the NaCl module using JSON message and provides
* Japanese input method using IME extension API (chrome.input.ime).
*
*/
'use strict';
/**
* Namespace for this extension.
* @suppress {const|duplicate}
*/
var mozc = window.mozc || {};
/**
* The candidate window size.
* @private {number}
*/
mozc.CANDIDATE_WINDOW_PAGE_SIZE_ = 9;
/**
* Enum of preedit method.
* This is same as mozc.commands.Output.PreeditMethod in command.proto.
* @enum {string}
* @private
*/
mozc.PreeditMethod_ = {
ROMAN: 'ROMAN',
KANA: 'KANA'
};
/**
* Enum of composition mode.
* This is same as mozc.commands.CompositionMode in command.proto.
* @enum {string}
* @private
*/
mozc.CompositionMode_ = {
DIRECT: 'DIRECT',
HIRAGANA: 'HIRAGANA',
FULL_KATAKANA: 'FULL_KATAKANA',
HALF_ASCII: 'HALF_ASCII',
FULL_ASCII: 'FULL_ASCII',
HALF_KATAKANA: 'HALF_KATAKANA'
};
/**
* Enum of menu item IDs.
* @enum {string}
* @private
*/
mozc.MenuItemId_ = {
MENU_COMPOSITION_HIRAGANA: 'MENU_COMPOSITION_HIRAGANA',
MENU_COMPOSITION_FULL_KATAKANA: 'MENU_COMPOSITION_FULL_KATAKANA',
MENU_COMPOSITION_FULL_ASCII: 'MENU_COMPOSITION_FULL_ASCII',
MENU_COMPOSITION_HALF_KATAKANA: 'MENU_COMPOSITION_HALF_KATAKANA',
MENU_COMPOSITION_HALF_ASCII: 'MENU_COMPOSITION_HALF_ASCII',
MENU_COMPOSITION_DIRECT: 'MENU_COMPOSITION_DIRECT'
};
/**
* Composition menu table.
* @const
* @private {!Array.<{menu: mozc.MenuItemId_,
* mode: mozc.CompositionMode_,
* labelId: string}>}
*/
mozc.COMPOSITION_MENU_TABLE_ = [
{
menu: mozc.MenuItemId_.MENU_COMPOSITION_HIRAGANA,
mode: mozc.CompositionMode_.HIRAGANA,
labelId: 'compositionModeHiragana'
},
{
menu: mozc.MenuItemId_.MENU_COMPOSITION_FULL_KATAKANA,
mode: mozc.CompositionMode_.FULL_KATAKANA,
labelId: 'compositionModeFullKatakana'
},
{
menu: mozc.MenuItemId_.MENU_COMPOSITION_FULL_ASCII,
mode: mozc.CompositionMode_.FULL_ASCII,
labelId: 'compositionModeFullAscii'
},
{
menu: mozc.MenuItemId_.MENU_COMPOSITION_HALF_KATAKANA,
mode: mozc.CompositionMode_.HALF_KATAKANA,
labelId: 'compositionModeHalfKatakana'
},
{
menu: mozc.MenuItemId_.MENU_COMPOSITION_HALF_ASCII,
mode: mozc.CompositionMode_.HALF_ASCII,
labelId: 'compositionModeHalfAscii'
},
{
menu: mozc.MenuItemId_.MENU_COMPOSITION_DIRECT,
mode: mozc.CompositionMode_.DIRECT,
labelId: 'compositionModeDirect'
}
];
/**
* NaclMozc with IME extension API.
* @param {!HTMLElement} naclModule DOM Element of NaCl module.
* @constructor
* @struct
* @const
*/
mozc.NaclMozc = function(naclModule) {
/**
* Context information which is provided by Chrome.
* @private {InputContext}
*/
this.context_ = null;
/**
* Session id of Mozc's session. This id is handled as uint64 in NaCl. But
* JavaScript can't handle uint64. So we handle it as string in JavaScript.
* @private {string}
*/
this.sessionID_ = '';
/**
* The list of candidates.
* This data structure is same as the 2nd argument of
* chrome.input.ime.setCandidates().
* @private {!Array.<{candidate: string,
* id: number,
* label: string,
* annotation: string,
* usage: {title: string, body: string}}>}
*/
this.candidates_ = [];
/**
* The candidate window properties.
* @private {!Object.<string, *>}
*/
this.candidateWindowProperties_ = {};
/**
* Array of callback functions.
* Callbacks are added in postMessage_ and removed in onModuleMessage_.
* @private {!Array.<function(!mozc.Command)|function(!mozc.Event)|undefined>}
*/
this.naclMessageCallbacks_ = [];
/**
* Array of callback functions which will be called when NaCl Mozc will be
* initialized.
* @private {!Array.<function()|undefined>}
*/
this.initializationCallbacks_ = [];
/**
* Keyboard layout. 'us' and 'jp' are supported.
* @private {string}
*/
this.keyboardLayout_ = 'us';
/**
* Preedit method.
* @private {mozc.PreeditMethod_}
*/
this.preeditMethod_ = mozc.PreeditMethod_.ROMAN;
/**
* Composition mode.
* @private {mozc.CompositionMode_}
*/
this.compositionMode_ = mozc.CompositionMode_.HIRAGANA;
/**
* Whether the NaCl module has been initialized or not.
* @private {boolean}
*/
this.isNaclInitialized_ = false;
/**
* Whether the JavaScript side code is handling an event or not.
* @private {boolean}
*/
this.isHandlingEvent_ = false;
/**
* Array of waiting event handlers.
* @private {!Array.<function()>}
*/
this.waitingEventHandlers_ = [];
/**
* Engine id which is passed from IME Extension API.
* @private {string}
*/
this.engine_id_ = '';
/**
* Key event translator.
* @private {!mozc.KeyTranslator}
*/
this.keyTranslator_ = new mozc.KeyTranslator();
/**
* callbackCommand_ stores the callback message which is recieved from NaCl
* module. This callback will be cancelled when the user presses the
* subsequent key. In the current implementation, if the subsequent key event
* also makes callback, the second callback will be called in the timimg of
* the first callback.
* @private {!mozc.Callback}
*/
this.callbackCommand_ = /** @type {!mozc.Callback} */ ({});
/**
* The surrounding information.
* @private {Object.<{text: string, focus: number, anchor: number}>}
*/
this.surroundingInfo_ = null;
/**
* DOM Element of NaCl module.
* @private {!HTMLElement}
*/
this.naclModule_ = naclModule;
this.naclModule_.addEventListener(
'load', this.onModuleLoad_.bind(this), true);
this.naclModule_.addEventListener(
'message', this.onModuleMessage_.bind(this), true);
this.naclModule_.addEventListener(
'progress', this.onModuleProgress_.bind(this), true);
this.naclModule_.addEventListener(
'error', this.onModuleError_.bind(this), true);
// Registers event handlers.
chrome.input.ime.onActivate.addListener(
this.wrapAsyncHandler_(this.onActivate_));
chrome.input.ime.onDeactivated.addListener(
this.wrapAsyncHandler_(this.onDeactivated_));
chrome.input.ime.onFocus.addListener(
this.wrapAsyncHandler_(this.onFocus_));
chrome.input.ime.onBlur.addListener(
this.wrapAsyncHandler_(this.onBlur_));
chrome.input.ime.onInputContextUpdate.addListener(
this.wrapAsyncHandler_(this.onInputContextUpdate_));
chrome.input.ime.onKeyEvent.addListener(
this.wrapAsyncHandler_(this.onKeyEventAsync_), ['async']);
chrome.input.ime.onCandidateClicked.addListener(
this.wrapAsyncHandler_(this.onCandidateClicked_));
chrome.input.ime.onMenuItemActivated.addListener(
this.wrapAsyncHandler_(this.onMenuItemActivated_));
// chrome.input.ime.onSurroundingTextChanged is available from ChromeOS 27.
if (chrome.input.ime.onSurroundingTextChanged) {
chrome.input.ime.onSurroundingTextChanged.addListener(
this.wrapAsyncHandler_(this.onSurroundingTextChanged_));
}
// chrome.input.ime.onReset is available from ChromeOS 29.
if (chrome.input.ime.onReset) {
chrome.input.ime.onReset.addListener(this.wrapAsyncHandler_(this.onReset_));
}
};
/**
* Calls the callback when NaCl Mozc is initialized
* @param {function()} callback Function to be called when NaCl Mozc is
* initialized.
*/
mozc.NaclMozc.prototype.callWhenInitialized = function(callback) {
if (this.isNaclInitialized_) {
callback();
return;
}
this.initializationCallbacks_.push(callback);
};
/**
* Creates a mozc.Command object with the specified input type.
* @param {string} type Input type of mozc.Command object.
* @return {!mozc.Command} Created mozc.Command.
* @private
*/
mozc.NaclMozc.prototype.createMozcCommand_ = function(type) {
return /** @type {!mozc.Command} */ ({'input': {'type': type}, 'output': {}});
};
/**
* Creates a mozc.Event object with the specified type.
* @param {string} type Type of mozc.Event object.
* @return {!mozc.Event} Created mozc.Event.
* @private
*/
mozc.NaclMozc.prototype.createMozcEvent_ = function(type) {
return /** @type {!mozc.Event} */ ({'type': type});
};
/**
* Creates a mozc.SessionCommand object with the specified type.
* @param {string} type Type of mozc.SessionCommand object.
* @return {!mozc.SessionCommand} Created mozc.SessionCommand.
* @private
*/
mozc.NaclMozc.prototype.createMozcSessionCommand_ = function(type) {
return /** @type {!mozc.SessionCommand} */ ({'type': type});
};
/**
* Sends RELOAD command to NaCl module.
* @param {function(!mozc.Command)=} opt_callback Function to be called with
* results from NaCl module.
*/
mozc.NaclMozc.prototype.sendReload = function(opt_callback) {
this.postMozcCommand_(this.createMozcCommand_('RELOAD'), opt_callback);
};
/**
* Sends GET_CONFIG command to NaCl module.
* @param {function(!mozc.Command)=} opt_callback Function to be called with
* results from NaCl module.
*/
mozc.NaclMozc.prototype.getConfig = function(opt_callback) {
this.postMozcCommand_(this.createMozcCommand_('GET_CONFIG'), opt_callback);
};
/**
* Sets config and sends SET_CONFIG command to NaCl module.
* @param {!Object.<string,*>} config Config object to be set to.
* @param {function(!mozc.Command)=} opt_callback Function to be called with
* results from NaCl module.
*/
mozc.NaclMozc.prototype.setConfig = function(config, opt_callback) {
if (config['preedit_method']) {
this.setPreeditMethod(
/** @type {mozc.PreeditMethod_} */ (config['preedit_method']));
}
var mozcCommand = this.createMozcCommand_('SET_CONFIG');
mozcCommand.input.config = config;
this.postMozcCommand_(mozcCommand, opt_callback);
};
/**
* Sends CLEAR_USER_HISTORY command to NaCl module.
* @param {function(!mozc.Command)=} opt_callback Function to be called with
* results from NaCl module.
*/
mozc.NaclMozc.prototype.clearUserHistory = function(opt_callback) {
this.postMozcCommand_(this.createMozcCommand_('CLEAR_USER_HISTORY'),
opt_callback);
};
/**
* Sends CLEAR_USER_PREDICTION command to NaCl module.
* @param {function(!mozc.Command)=} opt_callback Function to be called with
* results from NaCl module.
*/
mozc.NaclMozc.prototype.clearUserPrediction = function(opt_callback) {
this.postMozcCommand_(this.createMozcCommand_('CLEAR_USER_PREDICTION'),
opt_callback);
};
/**
* Sets preedit method
* @param {mozc.PreeditMethod_} newMethod The new preedit method to be set to.
*/
mozc.NaclMozc.prototype.setPreeditMethod = function(newMethod) {
for (var key in mozc.PreeditMethod_) {
if (newMethod == mozc.PreeditMethod_[key]) {
this.preeditMethod_ = mozc.PreeditMethod_[key];
return;
}
}
console.error('Preedit method ' + newMethod + ' is not supported.');
};
/**
* Sends SEND_USER_DICTIONARY_COMMAND command to NaCl module.
* @param {!mozc.UserDictionaryCommand} command User dictionary command object
* to be sent.
* @param {function(!mozc.UserDictionaryCommandStatus)=} opt_callback Function
* to be called with results from NaCl module.
*/
mozc.NaclMozc.prototype.sendUserDictionaryCommand = function(command,
opt_callback) {
var mozcCommand = this.createMozcCommand_('SEND_USER_DICTIONARY_COMMAND');
mozcCommand.input.user_dictionary_command = command;
this.postMozcCommand_(
mozcCommand,
opt_callback ?
/**
* @this {!mozc.NaclMozc}
* @param {function(!mozc.UserDictionaryCommandStatus)} callback
* @param {!mozc.Command} response
*/
(function(callback, response) {
if (response.output.user_dictionary_command_status == undefined) {
console.log('sendUserDictionaryCommand error');
return;
}
callback(response.output.user_dictionary_command_status);
}).bind(this, opt_callback) :
undefined);
};
/**
* Gets the version information of NaCl Mozc module.
* @param {function(!mozc.Event)} callback Function to be called with results
* from NaCl module.
*/
mozc.NaclMozc.prototype.getVersionInfo = function(callback) {
this.postNaclMozcEvent_(this.createMozcEvent_('GetVersionInfo'), callback);
};
/**
* Gets POS list from NaCl Mozc module.
* @param {function(!mozc.Event)} callback Function to be called with results
* from NaCl module.
*/
mozc.NaclMozc.prototype.getPosList = function(callback) {
this.postNaclMozcEvent_(this.createMozcEvent_('GetPosList'), callback);
};
/**
* Check if all characters in the given string is a legitimate character
* for reading.
* @param {string} text The string to check.
* @param {function(!mozc.Event)} callback Function to be called with results
* from NaCl module.
*/
mozc.NaclMozc.prototype.isValidReading = function(text, callback) {
var event = this.createMozcEvent_('IsValidReading');
event.data = text;
this.postNaclMozcEvent_(event, callback);
};
/**
* Sends callback command to NaCl module.
* @param {function(!mozc.Command)=} opt_callback Function to be called with
* results from NaCl module.
* @private
*/
mozc.NaclMozc.prototype.sendCallbackCommand_ = function(opt_callback) {
if (!this.sessionID_) {
console.error('Session has not been created.');
return;
}
if (!this.callbackCommand_.session_command) {
return;
}
var command = this.callbackCommand_.session_command;
if (command.type == 'CONVERT_REVERSE' && this.surroundingInfo_) {
if (this.surroundingInfo_.focus < this.surroundingInfo_.anchor) {
command.text = this.surroundingInfo_.text.substring(
this.surroundingInfo_.focus,
this.surroundingInfo_.anchor);
} else {
command.text = this.surroundingInfo_.text.substring(
this.surroundingInfo_.anchor,
this.surroundingInfo_.focus);
}
}
this.callbackCommand_ = /** @type {!mozc.Callback} */ ({});
this.postMozcSessionCommand_(command, opt_callback);
};
/**
* Wraps event handler to be able to managed by waitingEventHandlers_.
* @param {!Function} handler Event handler.
* @return {!Function} Wraped Event handler.
* @private
*/
mozc.NaclMozc.prototype.wrapAsyncHandler_ = function(handler) {
return (/**
* @this {!mozc.NaclMozc}
* @param {!Function} handler
*/
(function(handler) {
this.waitingEventHandlers_.push(
Function.prototype.apply.bind(
handler,
this,
Array.prototype.slice.call(arguments, 1)));
this.executeWatingEventHandlers_();
})).bind(this, handler);
};
/**
* Exexutes the waiting event handlers.
* This function is used to prevent the event handlers from being executed while
* the NaCl module is being initialized or another event handler is being
* executed or waiting for the callback from the NaCl module.
* @private
*/
mozc.NaclMozc.prototype.executeWatingEventHandlers_ = function() {
while (this.isNaclInitialized_ &&
!this.isHandlingEvent_ &&
!this.hasNaclMessageCallback_() &&
this.waitingEventHandlers_.length != 0) {
this.isHandlingEvent_ = true;
this.waitingEventHandlers_.shift()();
this.isHandlingEvent_ = false;
}
};
/**
* Returns true if naclMessageCallbacks_ has callback object.
* @return {boolean} naclMessageCallbacks_ has callback object or not.
* @private
*/
mozc.NaclMozc.prototype.hasNaclMessageCallback_ = function() {
for (var callbackId in this.naclMessageCallbacks_) {
return true;
}
return false;
};
/**
* Posts a message to NaCl module.
* @param {!mozc.Message} message Message object to be posted to NaCl module.
* @param {!(function(!mozc.Command)|function(!mozc.Event))=} opt_callback
* Function to be called with results from NaCl module.
* @private
*/
mozc.NaclMozc.prototype.postMessage_ = function(message, opt_callback) {
if (!this.isNaclInitialized_) {
console.error('NaCl module is not initialized yet.');
return;
}
var callbackId = 0;
while (callbackId in this.naclMessageCallbacks_) {
callbackId++;
}
this.naclMessageCallbacks_[callbackId] = opt_callback;
message.id = callbackId;
this.naclModule_['postMessage'](JSON.stringify(message));
};
/**
* Posts a message which wraps the Mozc command to NaCl module.
* @param {!mozc.Command} command Command object to be posted to NaCl module.
* @param {function(!mozc.Command)=} opt_callback Function to be called with
* results from NaCl module.
* @private
*/
mozc.NaclMozc.prototype.postMozcCommand_ = function(command, opt_callback) {
this.postMessage_(
/** @type {!mozc.Message} */ ({'cmd': command}),
opt_callback);
};
/**
* Posts an event which is spesific to NaCl Mozc such as 'SyncToFile'.
* @param {!mozc.Event} event Event object to be posted to NaCl module.
* @param {function(!mozc.Event)=} opt_callback Function to be called with
* results from NaCl module.
* @private
*/
mozc.NaclMozc.prototype.postNaclMozcEvent_ = function(event, opt_callback) {
this.postMessage_(
/** @type {!mozc.Message} */ ({'event': event}),
opt_callback);
};
/**
* Posts a SessionCommand to NaCl module.
* @param {!mozc.SessionCommand} sessionCommand SessionCommand object to be
* posted to NaCl module.
* @param {function(!mozc.Command)=} opt_callback Function to be called with
* results from NaCl module.
* @private
*/
mozc.NaclMozc.prototype.postMozcSessionCommand_ = function(sessionCommand,
opt_callback) {
var mozcCommand = this.createMozcCommand_('SEND_COMMAND');
mozcCommand.input.id = this.sessionID_;
mozcCommand.input.command = sessionCommand;
this.postMozcCommand_(mozcCommand, opt_callback);
};
/**
* Processes the response command from NaCl module.
* @param {!mozc.Command} mozcCommand Response command object from NaCl module.
* @private
*/
mozc.NaclMozc.prototype.processResponse_ = function(mozcCommand) {
if (!mozcCommand || !mozcCommand.output) {
return;
}
if (mozcCommand.output.deletion_range) {
// chrome.input.ime.deleteSurroundingText is available from ChromeOS 27.
if (chrome.input.ime.deleteSurroundingText) {
chrome.input.ime.deleteSurroundingText({
'engineID': this.engine_id_,
'contextID': this.context_.contextID,
'offset': mozcCommand.output.deletion_range.offset,
'length': mozcCommand.output.deletion_range.length
},
this.outputResponse_.bind(this, mozcCommand.output));
return;
}
}
this.outputResponse_(mozcCommand.output);
};
/**
* Outputs the response command from NaCl module.
* @param {!mozc.Output} output mozc.Output object from NaCl module.
* @private
*/
mozc.NaclMozc.prototype.outputResponse_ = function(output) {
this.updatePreedit_(output.preedit);
this.commitResult_(output.result);
this.updateCandidates_(output.candidates);
if (output.mode) {
var new_mode = output.mode;
if (this.compositionMode_ != new_mode) {
this.compositionMode_ = /** @type {mozc.CompositionMode_} */ (new_mode);
this.updateMenuItems_();
}
}
if (output.callback) {
this.callbackCommand_ = output.callback;
if (this.callbackCommand_.delay_millisec) {
window.setTimeout(
this.sendCallbackCommand_.bind(
this,
this.processResponse_.bind(this)),
this.callbackCommand_.delay_millisec);
} else {
this.sendCallbackCommand_(this.processResponse_.bind(this));
}
}
};
/**
* Updates preedit composition.
* @param {mozc.Preedit=} opt_mozcPreedit The preedit data passed from NaCl
* Mozc
* module.
* @private
*/
mozc.NaclMozc.prototype.updatePreedit_ = function(opt_mozcPreedit) {
if (!opt_mozcPreedit) {
chrome.input.ime.setComposition({
'contextID': this.context_.contextID,
'text': '',
'cursor': 0
});
return;
}
var preeditString = '';
var preeditSegments = [];
for (var i = 0; i < opt_mozcPreedit.segment.length; ++i) {
var segment = {
'start': preeditString.length,
'end': preeditString.length + opt_mozcPreedit.segment[i].value_length
};
if (opt_mozcPreedit.segment[i].annotation == 'UNDERLINE') {
segment.style = 'underline';
} else if (opt_mozcPreedit.segment[i].annotation == 'HIGHLIGHT') {
segment.style = 'doubleUnderline';
}
preeditSegments.push(segment);
preeditString += opt_mozcPreedit.segment[i].value;
}
// We do not use a cursor position obtained from Mozc. It is because the
// cursor position is used to locate the candidate window.
var cursor = 0;
if (opt_mozcPreedit.highlighted_position != undefined) {
cursor = opt_mozcPreedit.highlighted_position;
} else if (opt_mozcPreedit.cursor != undefined) {
cursor = opt_mozcPreedit.cursor;
}
chrome.input.ime.setComposition({
'contextID': this.context_.contextID,
'text': preeditString,
'cursor': cursor,
'segments': preeditSegments
});
};
/**
* Updates the candidate window properties.
* This method calls chrome.input.ime.setCandidateWindowProperties() with
* the properties which have been changed.
* @param {!Object.<string, *>} properties The new properties.
* @private
*/
mozc.NaclMozc.prototype.updateCandidateWindowProperties_ =
function(properties) {
var diffProperties = {};
var propertyNames = ['cursorVisible',
'vertical',
'pageSize',
'auxiliaryTextVisible',
'auxiliaryText',
'visible',
'windowPosition'];
var changed = false;
for (var i = 0; i < propertyNames.length; ++i) {
var name = propertyNames[i];
if (this.candidateWindowProperties_[name] != properties[name]) {
diffProperties[name] = properties[name];
this.candidateWindowProperties_[name] = properties[name];
changed = true;
}
}
if (changed) {
chrome.input.ime.setCandidateWindowProperties({
'engineID': this.engine_id_,
'properties': diffProperties
});
}
};
/**
* Checks two objects are the same objects.
* @param {!Object} object1 The first object to compare.
* @param {!Object} object2 The second object to compare.
* @return {boolean} Whether object1 and object2 are the same objects.
* @private
*/
mozc.NaclMozc.prototype.compareObjects_ = function(object1, object2) {
for (var key in object1) {
if (typeof(object1[key]) != typeof(object2[key])) {
return false;
}
if (object1[key]) {
if (typeof(object1[key]) == 'object') {
if (!this.compareObjects_(object1[key], object2[key])) {
return false;
}
} else if (typeof(object1[key]) == 'function') {
if (object1[key].toString() != object2[key].toString()) {
return false;
}
} else {
if (object1[key] !== object2[key]) {
return false;
}
}
} else {
if (object1[key] !== object2[key]) {
return false;
}
}
}
for (var key in object2) {
if (typeof(object1[key]) == 'undefined' &&
typeof(object2[key]) != 'undefined') {
return false;
}
}
return true;
};
/**
* Updates the candidate window.
* @param {mozc.Candidates=} opt_mozcCandidates The candidates data passed
* from NaCl Mozc module.
* @private
*/
mozc.NaclMozc.prototype.updateCandidates_ = function(opt_mozcCandidates) {
if (!opt_mozcCandidates) {
this.updateCandidateWindowProperties_(
{'visible': false, 'auxiliaryTextVisible': false});
return;
}
var focusedID = null;
var newCandidates = [];
var candidatesIdMap = {};
for (var i = 0; i < opt_mozcCandidates.candidate.length; ++i) {
var item = opt_mozcCandidates.candidate[i];
if (item.index == opt_mozcCandidates.focused_index) {
focusedID = item.id;
}
var newCandidate = {
'candidate': item.value,
'id': item.id
};
if (item.annotation) {
newCandidate.annotation = item.annotation.description;
newCandidate.label = item.annotation.shortcut;
}
newCandidates.push(newCandidate);
candidatesIdMap[item.id] = newCandidate;
}
if (opt_mozcCandidates.usages) {
for (var i = 0; i < opt_mozcCandidates.usages.information.length;
++i) {
var usage = opt_mozcCandidates.usages.information[i];
for (var j = 0; j < usage.candidate_id.length; ++j) {
if (candidatesIdMap[usage.candidate_id[j]]) {
candidatesIdMap[usage.candidate_id[j]].usage = {
'title': usage.title,
'body': usage.description
};
}
}
}
}
if (!this.compareObjects_(this.candidates_, newCandidates)) {
// Calls chrome.input.ime.setCandidates() if the candidates has changed.
chrome.input.ime.setCandidates({
'contextID': this.context_.contextID,
'candidates': newCandidates
});
this.candidates_ = newCandidates;
}
// We have to call setCursorPosition even if there is no focused candidate.
// This is because the last set cusor position will be used to scroll the page
// of the candidates if we don't set.
chrome.input.ime.setCursorPosition({
'contextID': this.context_.contextID,
'candidateID': (focusedID != null) ? focusedID : 0});
var auxiliaryText = '';
if (opt_mozcCandidates.footer) {
if (opt_mozcCandidates.footer.label != undefined) {
auxiliaryText = opt_mozcCandidates.footer.label;
} else if (opt_mozcCandidates.footer.sub_label != undefined) {
auxiliaryText = opt_mozcCandidates.footer.sub_label;
}
if (opt_mozcCandidates.footer.index_visible &&
opt_mozcCandidates.focused_index != undefined) {
if (auxiliaryText.length != 0) {
auxiliaryText += ' ';
}
auxiliaryText += (opt_mozcCandidates.focused_index + 1) + '/' +
opt_mozcCandidates.size;
}
}
var pageSize = mozc.CANDIDATE_WINDOW_PAGE_SIZE_;
if (opt_mozcCandidates.category == 'SUGGESTION') {
pageSize = Math.min(pageSize,
opt_mozcCandidates.candidate.length);
}
var windowPosition = (opt_mozcCandidates.category == 'SUGGESTION' ||
opt_mozcCandidates.category == 'PREDICTION') ?
'composition' : 'cursor';
this.updateCandidateWindowProperties_({
'visible': true,
'cursorVisible': (focusedID != null),
'vertical': true,
'pageSize': pageSize,
'auxiliaryTextVisible': (auxiliaryText.length != 0),
'auxiliaryText': auxiliaryText,
'windowPosition': windowPosition
});
};
/**
* Commits result string.
* @param {mozc.Result=} opt_mozcResult The result data passed from NaCl
* Mozc module.
* @private
*/
mozc.NaclMozc.prototype.commitResult_ = function(opt_mozcResult) {
if (!opt_mozcResult) {
return;
}
chrome.input.ime.commitText({
'contextID': this.context_.contextID,
'text': opt_mozcResult.value
});
};
/**
* Callback method called when IME is activated.
* @param {string} engineID ID of the engine.
* @private
*/
mozc.NaclMozc.prototype.onActivate_ = function(engineID) {
this.engine_id_ = engineID;
// Sets keyboardLayout_.
var getManifest = chrome.runtime.getManifest;
if (getManifest) {
var input_components = getManifest()['input_components'];
for (var i = 0; i < input_components.length; ++i) {
if (input_components[i].id == engineID) {
this.keyboardLayout_ = input_components[i]['layouts'][0];
}
}
}
this.updateMenuItems_();
};
/**
* Callback method called when IME is deactivated.
* @param {string} engineID ID of the engine.
* @private
*/
mozc.NaclMozc.prototype.onDeactivated_ = function(engineID) {
this.postNaclMozcEvent_(this.createMozcEvent_('SyncToFile'));
};
/**
* Callback method called when a context acquires a focus.
* @param {!InputContext} context The context information.
* @private
*/
mozc.NaclMozc.prototype.onFocus_ = function(context) {
this.context_ = context;
var mozcCommand = this.createMozcCommand_('CREATE_SESSION');
mozcCommand.input.capability =
/** @type {!mozc.Capability} */
({text_deletion: 'DELETE_PRECEDING_TEXT'});
mozcCommand.input.application_info =
/** @type {!mozc.ApplicationInfo} */
({timezone_offset: -(new Date()).getTimezoneOffset() * 60});
this.postMozcCommand_(
mozcCommand,
/**
* @this {!mozc.NaclMozc}
* @param {!mozc.Command} response
*/
(function(response) {
if (response.output.id == undefined) {
console.error('CREATE_SESSION error');
return;
}
this.sessionID_ = response.output.id;
}).bind(this));
};
/**
* Callback method called when a context lost a focus.
* @param {number} contextID ID of the context.
* @private
*/
mozc.NaclMozc.prototype.onBlur_ = function(contextID) {
if (!this.sessionID_) {
console.error('Session has not been created.');
return;
}
var mozcCommand = this.createMozcCommand_('DELETE_SESSION');
mozcCommand.input.id = this.sessionID_;
this.postMozcCommand_(mozcCommand, this.processResponse_.bind(this));
this.sessionID_ = '';
};
/**
* Callback method called when properties of the context is changed.
* @param {!InputContext} context context information.
* @private
*/
mozc.NaclMozc.prototype.onInputContextUpdate_ = function(context) {
// TODO(horo): Notify this event to Mozc since input field type may be changed
// to password field.
this.context_ = context;
};
/**
* Callback method called when IME catches a new key event.
* @param {string} engineID ID of the engine.
* @param {!ChromeKeyboardEvent} keyData key event data.
* @private
*/
mozc.NaclMozc.prototype.onKeyEventAsync_ = function(engineID, keyData) {
if (!this.sessionID_) {
console.error('Session has not been created.');
chrome.input.ime.keyEventHandled(keyData.requestId, false);
return;
}
var keyEvent =
this.keyTranslator_.translateKeyEvent(keyData,
this.preeditMethod_ == 'KANA',
this.keyboardLayout_);
if (this.compositionMode_ == 'DIRECT') {
// TODO(horo): Support custom keymap table.
if (keyEvent.special_key == 'HANKAKU' ||
keyEvent.special_key == 'HENKAN') {
chrome.input.ime.keyEventHandled(keyData.requestId, true);
this.switchCompositionMode_(mozc.CompositionMode_.HIRAGANA);
this.updateMenuItems_();
} else {
chrome.input.ime.keyEventHandled(keyData.requestId, false);
}
return;
}
if (keyEvent.key_code == undefined &&
keyEvent.special_key == undefined &&
keyEvent.modifier_keys == undefined) {
chrome.input.ime.keyEventHandled(keyData.requestId, false);
return;
}
// Cancels the callback request.
this.callbackCommand_ = /** @type {!mozc.Callback} */ ({});
keyEvent.mode = this.compositionMode_;
var context = /** @type {!mozc.Context} */ ({});
if (this.surroundingInfo_) {
context.preceding_text =
this.surroundingInfo_.text.substring(
0,
Math.min(this.surroundingInfo_.focus,
this.surroundingInfo_.anchor));
context.following_text =
this.surroundingInfo_.text.substring(
Math.max(this.surroundingInfo_.focus,
this.surroundingInfo_.anchor));
}
var mozcCommand = this.createMozcCommand_('SEND_KEY');
mozcCommand.input.id = this.sessionID_;
mozcCommand.input.key = keyEvent;
mozcCommand.input.context = context;
this.postMozcCommand_(
mozcCommand,
/**
* @this {!mozc.NaclMozc}
* @param {string} requestId
* @param {!mozc.Command} response
*/
(function(requestId, response) {
this.processResponse_(response);
chrome.input.ime.keyEventHandled(
requestId,
!!response.output.consumed);
}).bind(this, keyData.requestId));
return;
};
/**
* Callback method called when candidates on candidate window is clicked.
* @param {string} engineID ID of the engine.
* @param {number} candidateID Index of the candidate.
* @param {string} button Which mouse button was clicked.
* @private
*/
mozc.NaclMozc.prototype.onCandidateClicked_ = function(engineID,
candidateID,
button) {
var sessionCommand = this.createMozcSessionCommand_('SELECT_CANDIDATE');
sessionCommand.id = candidateID;
this.postMozcSessionCommand_(sessionCommand,
this.processResponse_.bind(this));
};
/**
* Callback method called when menu item on uber tray is activated.
* @param {string} engineID ID of the engine.
* @param {string} name name of the menu item.
* @private
*/
mozc.NaclMozc.prototype.onMenuItemActivated_ = function(engineID, name) {
for (var i = 0; i < mozc.COMPOSITION_MENU_TABLE_.length; ++i) {
if (name == mozc.COMPOSITION_MENU_TABLE_[i].menu) {
this.switchCompositionMode_(mozc.COMPOSITION_MENU_TABLE_[i].mode);
this.updateMenuItems_();
return;
}
}
console.error('Menu item ' + name + ' is not supported.');
};
/**
* Callback method called the editable string around caret is changed or when
* the caret position is moved. The text length is limited to 100 characters for
* each back and forth direction.
* @param {string} engineID ID of the engine.
* @param {!Object.<{text: string, focus: number, anchor: number}>} info The
* surrounding information.
* @private
*/
mozc.NaclMozc.prototype.onSurroundingTextChanged_ = function(engineID, info) {
this.surroundingInfo_ = info;
};
/**
* Callback method called when Chrome terminates ongoing text input session.
* @param {string} engineID ID of the engine.
* @private
*/
mozc.NaclMozc.prototype.onReset_ = function(engineID) {
if (!this.sessionID_) {
console.error('Session has not been created.');
return;
}
this.postMozcSessionCommand_(
this.createMozcSessionCommand_('RESET_CONTEXT'),
/**
* @this {!mozc.NaclMozc}
* @param {!mozc.Command} response
*/
(function(response) {
if (response.output.mode) {
// We ignore the mode in the response, because we should still use
// the current compositionMode_.
response.output.mode = undefined;
}
this.processResponse_(response);
}).bind(this));
};
/**
* Switches the composition mode.
* @param {mozc.CompositionMode_} newMode The new composition mode to be set.
* @private
*/
mozc.NaclMozc.prototype.switchCompositionMode_ = function(newMode) {
var lastMode = this.compositionMode_;
if (lastMode != mozc.CompositionMode_.DIRECT &&
newMode == mozc.CompositionMode_.DIRECT) {
var mozcCommand = this.createMozcCommand_('SEND_KEY');
mozcCommand.input.id = this.sessionID_;
mozcCommand.input.key =
/** @type {!mozc.KeyEvent} */ ({'special_key': 'OFF'});
this.postMozcCommand_(mozcCommand);
} else if (newMode != mozc.CompositionMode_.DIRECT) {
if (lastMode == mozc.CompositionMode_.DIRECT) {
var mozcCommand = this.createMozcCommand_('SEND_KEY');
mozcCommand.input.id = this.sessionID_;
mozcCommand.input.key =
/** @type {!mozc.KeyEvent} */ ({'special_key': 'ON'});
this.postMozcCommand_(
mozcCommand,
/**
* @this {!mozc.NaclMozc}
* @param {!mozc.CompositionMode_} newMode
* @param {!mozc.Command} response
*/
(function(newMode, response) {
var sessionCommand =
this.createMozcSessionCommand_('SWITCH_INPUT_MODE');
sessionCommand.composition_mode = newMode;
this.postMozcSessionCommand_(sessionCommand);
}).bind(this, newMode));
} else {
var sessionCommand =
this.createMozcSessionCommand_('SWITCH_INPUT_MODE');
sessionCommand.composition_mode = newMode;
this.postMozcSessionCommand_(sessionCommand);
}
}
this.compositionMode_ = newMode;
};
/**
* Updates the menu items
* @private
*/
mozc.NaclMozc.prototype.updateMenuItems_ = function() {
var menuItems = [];
for (var i = 0; i < mozc.COMPOSITION_MENU_TABLE_.length; ++i) {
menuItems.push({
'id': mozc.COMPOSITION_MENU_TABLE_[i].menu,
'label': chrome.i18n.getMessage(mozc.COMPOSITION_MENU_TABLE_[i].labelId),
'checked': this.compositionMode_ == mozc.COMPOSITION_MENU_TABLE_[i].mode,
'enabled': true,
'visible': true
});
}
chrome.input.ime.setMenuItems({
engineID: this.engine_id_,
'items': menuItems
});
};
/**
* Callback method called when the NaCl module has loaded.
* @private
*/
mozc.NaclMozc.prototype.onModuleLoad_ = function() {
};
/**
* Handler of the messages from the NaCl module.
* @param {Object} message message object from the NaCl module.
* @private
*/
mozc.NaclMozc.prototype.onModuleMessage_ = function(message) {
if (!message) {
console.error('onModuleMessage_ error');
return;
}
var mozcResponse = {};
try {
mozcResponse = /** @type {!mozc.Message} */ (JSON.parse(message.data));
} catch (e) {
console.error('handleMessage: Error: ' + e.message);
return;
}
if (mozcResponse.event &&
mozcResponse.event.type == 'InitializeDone') {
this.isNaclInitialized_ = true;
if (mozcResponse.event.config['preedit_method']) {
this.setPreeditMethod(
/** @type {mozc.PreeditMethod_} */
(mozcResponse.event.config['preedit_method']));
}
while (this.initializationCallbacks_.length > 0) {
this.initializationCallbacks_.shift()();
}
this.executeWatingEventHandlers_();
return;
}
var callback = this.naclMessageCallbacks_[mozcResponse.id];
delete this.naclMessageCallbacks_[mozcResponse.id];
if (callback) {
if (mozcResponse.cmd) {
callback(mozcResponse.cmd);
} else if (mozcResponse.event) {
callback(mozcResponse.event);
}
}
this.executeWatingEventHandlers_();
};
/**
* Progress event handler of the NaCl module.
* @param {Object} event Progress event object from the NaCl module.
* @private
*/
mozc.NaclMozc.prototype.onModuleProgress_ = function(event) {
if (!event) {
console.error('progress event error: event is null');
return;
}
if (event.lengthComputable && event.total > 0) {
console.log('Loading: ' + (event.loaded / event.total * 100) + '%');
} else {
console.log('Loading...');
}
};
/**
* Error event handler of the NaCl module.
* @private
*/
mozc.NaclMozc.prototype.onModuleError_ = function() {
console.error('onModuleError', this.naclModule_['lastError']);
};
/**
* New option page.
* @param {!Window} optionWindow Window object of the option page.
* @return {!mozc.OptionPage} Option page object.
*/
mozc.NaclMozc.prototype.newOptionPage = function(optionWindow) {
var optionPage = new mozc.OptionPage(this, optionWindow);
optionPage.initialize();
return optionPage;
};