blob: 3f940ed002d7104539a3b7a9dbe453b9e694325e [file] [log] [blame]
// Copyright 2014 The ChromeOS IME Authors. All Rights Reserved.
// limitations under the License.
// See the License for the specific language governing permissions and
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// distributed under the License is distributed on an "AS-IS" BASIS,
// Unless required by applicable law or agreed to in writing, software
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// You may obtain a copy of the License at
// you may not use this file except in compliance with the License.
// Licensed under the Apache License, Version 2.0 (the "License");
//
// Copyright 2013 The ChromeOS VK Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Definition of Model class.
* It is responsible for dynamically loading the layout JS files. It
* interprets the layout info and provides the function of getting
* transformed chars and recording history states to Model.
* It notifies View via events when layout info changes.
* This is the Model of MVC pattern.
*/
goog.provide('i18n.input.chrome.vk.Model');
goog.require('goog.events.EventTarget');
goog.require('goog.net.jsloader');
goog.require('goog.object');
goog.require('goog.string');
goog.require('i18n.input.chrome.vk.EventType');
goog.require('i18n.input.chrome.vk.LayoutEvent');
goog.require('i18n.input.chrome.vk.ParsedLayout');
/**
* Creates the Model object.
*
* @constructor
* @extends {goog.events.EventTarget}
*/
i18n.input.chrome.vk.Model = function() {
goog.base(this);
/**
* The registered layouts object.
* Its format is {<layout code>: <parsed layout obj>}.
*
* @type {!Object.<!i18n.input.chrome.vk.ParsedLayout|boolean>}
* @private
*/
this.layouts_ = {};
/**
* The active layout code.
*
* @type {string}
* @private
*/
this.activeLayout_ = '';
/**
* The layout code of which the layout is "being activated" when the layout
* hasn't been loaded yet.
*
* @type {string}
* @private
*/
this.delayActiveLayout_ = '';
/**
* History state used for ambiguous transforms.
*
* @type {!Object}
* @private
*/
this.historyState_ = {
previous: {text: '', transat: -1},
ambi: '',
current: {text: '', transat: -1}
};
// Exponses the onLayoutLoaded so that the layout JS can call it back.
goog.exportSymbol('cros_vk_loadme', goog.bind(this.onLayoutLoaded_, this));
};
goog.inherits(i18n.input.chrome.vk.Model, goog.events.EventTarget);
/**
* Loads the layout in the background.
*
* @param {string} layoutCode The layout will be loaded.
*/
i18n.input.chrome.vk.Model.prototype.loadLayout = function(layoutCode) {
if (!layoutCode) return;
var parsedLayout = this.layouts_[layoutCode];
// The layout is undefined means not loaded, false means loading.
if (parsedLayout == undefined) {
this.layouts_[layoutCode] = false;
i18n.input.chrome.vk.Model.loadLayoutScript_(layoutCode);
} else if (parsedLayout) {
this.dispatchEvent(new i18n.input.chrome.vk.LayoutEvent(
i18n.input.chrome.vk.EventType.LAYOUT_LOADED,
/** @type {!Object} */ (parsedLayout)));
}
};
/**
* Activate layout by setting the current layout.
*
* @param {string} layoutCode The layout will be set as current layout.
*/
i18n.input.chrome.vk.Model.prototype.activateLayout = function(
layoutCode) {
if (!layoutCode) return;
if (this.activeLayout_ != layoutCode) {
var parsedLayout = this.layouts_[layoutCode];
if (parsedLayout) {
this.activeLayout_ = layoutCode;
this.delayActiveLayout_ = '';
this.clearHistory();
} else if (parsedLayout == false) { // Layout being loaded?
this.delayActiveLayout_ = layoutCode;
}
}
};
/**
* Gets the current layout.
*
* @return {string} The current layout code.
*/
i18n.input.chrome.vk.Model.prototype.getCurrentLayout = function() {
return this.activeLayout_;
};
/**
* Predicts whether there would be future transforms for the history text.
*
* @return {number} The matched position. Returns -1 for no match.
*/
i18n.input.chrome.vk.Model.prototype.predictHistory = function() {
if (!this.activeLayout_ || !this.layouts_[this.activeLayout_]) {
return -1;
}
var parsedLayout = this.layouts_[this.activeLayout_];
var history = this.historyState_;
var text, transat;
if (history.ambi) {
text = history.previous.text;
transat = history.previous.transat;
// Tries to predict transform for previous history.
if (transat > 0) {
text = text.slice(0, transat) + '\u001d' + text.slice(transat) +
history.ambi;
} else {
text += history.ambi;
}
if (parsedLayout.predictTransform(text) >= 0) {
// If matched previous history, always return 0 because outside will use
// this to keep the composition text.
return 0;
}
}
// Tries to predict transform for current history.
text = history.current.text;
transat = history.current.transat;
if (transat >= 0) {
text = text.slice(0, transat) + '\u001d' + text.slice(transat);
}
var pos = parsedLayout.predictTransform(text);
if (transat >= 0 && pos > transat) {
// Adjusts the pos for removing the temporary \u001d character.
pos--;
}
return pos;
};
/**
* Translates the key code into the chars to put into the active input box.
*
* @param {string} chars The key commit chars.
* @param {string} charsBeforeCaret The chars before the caret in the active
* input box. This will be used to compare with the history states.
* @return {Object} The replace chars object whose 'back' means delete how many
* chars back from the caret, and 'chars' means the string insert after the
* deletion. Returns null if no result.
*/
i18n.input.chrome.vk.Model.prototype.translate = function(
chars, charsBeforeCaret) {
if (!this.activeLayout_ || !chars) {
return null;
}
var parsedLayout = this.layouts_[this.activeLayout_];
if (!parsedLayout) {
return null;
}
this.matchHistory_(charsBeforeCaret);
var result, history = this.historyState_;
if (history.ambi) {
// If ambi is not empty, it means some ambi chars has been typed
// before. e.g. ka->k, kaa->K, typed 'ka', and now typing 'a':
// history.previous == 'k',1
// history.current == 'k',1
// history.ambi == 'a'
// So now we should get transform of 'k\u001d' + 'aa'.
result = parsedLayout.transform(
history.previous.text, history.previous.transat,
history.ambi + chars);
// Note: result.back could be negative number. In such case, we should give
// up the transform result. This is to be compatible the old vk behaviors.
if (result && result.back < 0) {
result = null;
}
}
if (result) {
// Because the result is related to previous history, adjust the result so
// that it is related to current history.
var prev = history.previous.text;
prev = prev.slice(0, prev.length - result.back);
prev += result.chars;
result.back = history.current.text.length;
result.chars = prev;
} else {
// If no ambi chars or no transforms for ambi chars, try to match the
// regular transforms. In above case, if now typing 'b', we should get
// transform of 'k\u001d' + 'b'.
result = parsedLayout.transform(
history.current.text, history.current.transat, chars);
}
// Updates the history state.
if (parsedLayout.isAmbiChars(history.ambi + chars)) {
if (!history.ambi) {
// Empty ambi means chars should be the first ambi chars.
// So now we should set the previous.
history.previous = goog.object.clone(history.current);
}
history.ambi += chars;
} else if (parsedLayout.isAmbiChars(chars)) {
// chars could match ambi regex when ambi+chars cannot.
// In this case, record the current history to previous, and set ambi as
// chars.
history.previous = goog.object.clone(history.current);
history.ambi = chars;
} else {
history.previous.text = '';
history.previous.transat = -1;
history.ambi = '';
}
// Updates the history text per transform result.
var text = history.current.text;
var transat = history.current.transat;
if (result) {
text = text.slice(0, text.length - result.back);
text += result.chars;
transat = text.length;
} else {
text += chars;
// This function doesn't return null. So if result is null, fill it.
result = {back: 0, chars: chars};
}
// The history text cannot cannot contain SPACE!
var spacePos = text.lastIndexOf(' ');
if (spacePos >= 0) {
text = text.slice(spacePos + 1);
if (transat > spacePos) {
transat -= spacePos + 1;
} else {
transat = -1;
}
}
history.current.text = text;
history.current.transat = transat;
return result;
};
/**
* Wether the active layout has transforms defined.
*
* @return {boolean} True if transforms defined, false otherwise.
*/
i18n.input.chrome.vk.Model.prototype.hasTransforms = function() {
var parsedLayout = this.layouts_[this.activeLayout_];
return !!parsedLayout && !!parsedLayout.transforms;
};
/**
* Processes the backspace key. It affects the history state.
*
* @param {string} charsBeforeCaret The chars before the caret in the active
* input box. This will be used to compare with the history states.
*/
i18n.input.chrome.vk.Model.prototype.processBackspace = function(
charsBeforeCaret) {
this.matchHistory_(charsBeforeCaret);
var history = this.historyState_;
// Reverts the current history. If the backspace across over the transat pos,
// clean it up.
var text = history.current.text;
if (text) {
text = text.slice(0, text.length - 1);
history.current.text = text;
if (history.current.transat > text.length) {
history.current.transat = text.length;
}
text = history.ambi;
if (text) { // If there is ambi text, remove the last char in ambi.
history.ambi = text.slice(0, text.length - 1);
}
// Prev history only exists when ambi is not empty.
if (!history.ambi) {
history.previous = {text: '', transat: -1};
}
} else {
// Cleans up the previous history.
history.previous = {text: '', transat: -1};
history.ambi = '';
// Cleans up the current history.
history.current = goog.object.clone(history.previous);
}
};
/**
* Callback when layout loaded.
*
* @param {!Object} layout The layout object passed from the layout JS's loadme
* callback.
* @private
*/
i18n.input.chrome.vk.Model.prototype.onLayoutLoaded_ = function(layout) {
var parsedLayout = new i18n.input.chrome.vk.ParsedLayout(layout);
if (parsedLayout.id) {
this.layouts_[parsedLayout.id] = parsedLayout;
}
if (this.delayActiveLayout_ == layout.id) {
this.activateLayout(this.delayActiveLayout_);
this.delayActiveLayout_ = '';
}
this.dispatchEvent(new i18n.input.chrome.vk.LayoutEvent(
i18n.input.chrome.vk.EventType.LAYOUT_LOADED, parsedLayout));
};
/**
* Matches the given text to the last transformed text. Clears history if they
* are not matched.
*
* @param {string} text The text to be matched.
* @private
*/
i18n.input.chrome.vk.Model.prototype.matchHistory_ = function(text) {
var hisText = this.historyState_.current.text;
if (!hisText || !text || !(goog.string.endsWith(text, hisText) ||
goog.string.endsWith(hisText, text))) {
this.clearHistory();
}
};
/**
* Clears the history state.
*/
i18n.input.chrome.vk.Model.prototype.clearHistory = function() {
this.historyState_.ambi = '';
this.historyState_.previous = {text: '', transat: -1};
this.historyState_.current = goog.object.clone(this.historyState_.previous);
};
/**
* Prunes the history state to remove a number of chars at beginning.
*
* @param {number} count The count of chars to be removed.
*/
i18n.input.chrome.vk.Model.prototype.pruneHistory = function(count) {
var pruneFunc = function(his) {
his.text = his.text.slice(count);
if (his.transat > 0) {
his.transat -= count;
if (his.transat <= 0) {
his.transat = -1;
}
}
};
pruneFunc(this.historyState_.previous);
pruneFunc(this.historyState_.current);
};
/**
* Loads the script for a layout.
*
* @param {string} layoutCode The layout code.
* @private
*/
i18n.input.chrome.vk.Model.loadLayoutScript_ = function(layoutCode) {
goog.net.jsloader.load('layouts/' + layoutCode + '.js');
};