| // 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'); |
| }; |