Index: third_party/google_input_tools/src/chrome/os/keyboard/model.js |
diff --git a/third_party/google_input_tools/src/chrome/os/keyboard/model.js b/third_party/google_input_tools/src/chrome/os/keyboard/model.js |
new file mode 100644 |
index 0000000000000000000000000000000000000000..3f940ed002d7104539a3b7a9dbe453b9e694325e |
--- /dev/null |
+++ b/third_party/google_input_tools/src/chrome/os/keyboard/model.js |
@@ -0,0 +1,424 @@ |
+// 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'); |
+}; |