Update mojo sdk to rev 1dc8a9a5db73d3718d99917fadf31f5fb2ebad4f
[chromium-blink-merge.git] / third_party / google_input_tools / src / chrome / os / keyboard / model.js
blobeb8470af8c4c926638f6eb532562aa84cf9ced9a
1 // Copyright 2013 The ChromeOS IME Authors. All Rights Reserved.
2 // limitations under the License.
3 // See the License for the specific language governing permissions and
4 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5 // distributed under the License is distributed on an "AS-IS" BASIS,
6 // Unless required by applicable law or agreed to in writing, software
7 //
8 //      http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // You may obtain a copy of the License at
11 // you may not use this file except in compliance with the License.
12 // Licensed under the Apache License, Version 2.0 (the "License");
15 /**
16  * @fileoverview Definition of Model class.
17  *     It is responsible for dynamically loading the layout JS files. It
18  *     interprets the layout info and provides the function of getting
19  *     transformed chars and recording history states to Model.
20  *     It notifies View via events when layout info changes.
21  *     This is the Model of MVC pattern.
22  */
24 goog.provide('i18n.input.chrome.vk.Model');
26 goog.require('goog.events.EventTarget');
27 goog.require('goog.net.jsloader');
28 goog.require('goog.object');
29 goog.require('goog.string');
30 goog.require('i18n.input.chrome.vk.EventType');
31 goog.require('i18n.input.chrome.vk.LayoutEvent');
32 goog.require('i18n.input.chrome.vk.ParsedLayout');
36 /**
37  * Creates the Model object.
38  *
39  * @constructor
40  * @extends {goog.events.EventTarget}
41  */
42 i18n.input.chrome.vk.Model = function() {
43   goog.base(this);
45   /**
46    * The registered layouts object.
47    * Its format is {<layout code>: <parsed layout obj>}.
48    *
49    * @type {!Object.<!i18n.input.chrome.vk.ParsedLayout|boolean>}
50    * @private
51    */
52   this.layouts_ = {};
54   /**
55    * The active layout code.
56    *
57    * @type {string}
58    * @private
59    */
60   this.activeLayout_ = '';
62   /**
63    * The layout code of which the layout is "being activated" when the layout
64    * hasn't been loaded yet.
65    *
66    * @type {string}
67    * @private
68    */
69   this.delayActiveLayout_ = '';
71   /**
72    * History state used for ambiguous transforms.
73    *
74    * @type {!Object}
75    * @private
76    */
77   this.historyState_ = {
78     previous: {text: '', transat: -1},
79     ambi: '',
80     current: {text: '', transat: -1}
81   };
83   // Exponses the onLayoutLoaded so that the layout JS can call it back.
84   goog.exportSymbol('cros_vk_loadme', goog.bind(this.onLayoutLoaded_, this));
86 goog.inherits(i18n.input.chrome.vk.Model, goog.events.EventTarget);
89 /**
90  * Loads the layout in the background.
91  *
92  * @param {string} layoutCode The layout will be loaded.
93  */
94 i18n.input.chrome.vk.Model.prototype.loadLayout = function(layoutCode) {
95   if (!layoutCode) return;
97   var parsedLayout = this.layouts_[layoutCode];
98   // The layout is undefined means not loaded, false means loading.
99   if (parsedLayout == undefined) {
100     this.layouts_[layoutCode] = false;
101     i18n.input.chrome.vk.Model.loadLayoutScript_(layoutCode);
102   } else if (parsedLayout) {
103     this.dispatchEvent(new i18n.input.chrome.vk.LayoutEvent(
104         i18n.input.chrome.vk.EventType.LAYOUT_LOADED,
105         /** @type {!Object} */ (parsedLayout)));
106   }
111  * Activate layout by setting the current layout.
113  * @param {string} layoutCode The layout will be set as current layout.
114  */
115 i18n.input.chrome.vk.Model.prototype.activateLayout = function(
116     layoutCode) {
117   if (!layoutCode) return;
119   if (this.activeLayout_ != layoutCode) {
120     var parsedLayout = this.layouts_[layoutCode];
121     if (parsedLayout) {
122       this.activeLayout_ = layoutCode;
123       this.delayActiveLayout_ = '';
124       this.clearHistory();
125     } else if (parsedLayout == false) { // Layout being loaded?
126       this.delayActiveLayout_ = layoutCode;
127     }
128   }
133  * Gets the current layout.
135  * @return {string} The current layout code.
136  */
137 i18n.input.chrome.vk.Model.prototype.getCurrentLayout = function() {
138   return this.activeLayout_;
143  * Predicts whether there would be future transforms for the history text.
145  * @return {number} The matched position. Returns -1 for no match.
146  */
147 i18n.input.chrome.vk.Model.prototype.predictHistory = function() {
148   if (!this.activeLayout_ || !this.layouts_[this.activeLayout_]) {
149     return -1;
150   }
151   var parsedLayout = this.layouts_[this.activeLayout_];
152   var history = this.historyState_;
153   var text, transat;
154   if (history.ambi) {
155     text = history.previous.text;
156     transat = history.previous.transat;
157     // Tries to predict transform for previous history.
158     if (transat > 0) {
159       text = text.slice(0, transat) + '\u001d' + text.slice(transat) +
160           history.ambi;
161     } else {
162       text += history.ambi;
163     }
164     if (parsedLayout.predictTransform(text) >= 0) {
165       // If matched previous history, always return 0 because outside will use
166       // this to keep the composition text.
167       return 0;
168     }
169   }
170   // Tries to predict transform for current history.
171   text = history.current.text;
172   transat = history.current.transat;
173   if (transat >= 0) {
174     text = text.slice(0, transat) + '\u001d' + text.slice(transat);
175   }
176   var pos = parsedLayout.predictTransform(text);
177   if (transat >= 0 && pos > transat) {
178     // Adjusts the pos for removing the temporary \u001d character.
179     pos--;
180   }
181   return pos;
186  * Translates the key code into the chars to put into the active input box.
188  * @param {string} chars The key commit chars.
189  * @param {string} charsBeforeCaret The chars before the caret in the active
190  *     input box. This will be used to compare with the history states.
191  * @return {Object} The replace chars object whose 'back' means delete how many
192  *     chars back from the caret, and 'chars' means the string insert after the
193  *     deletion. Returns null if no result.
194  */
195 i18n.input.chrome.vk.Model.prototype.translate = function(
196     chars, charsBeforeCaret) {
197   if (!this.activeLayout_ || !chars) {
198     return null;
199   }
200   var parsedLayout = this.layouts_[this.activeLayout_];
201   if (!parsedLayout) {
202     return null;
203   }
205   this.matchHistory_(charsBeforeCaret);
206   var result, history = this.historyState_;
207   if (history.ambi) {
208     // If ambi is not empty, it means some ambi chars has been typed
209     // before. e.g. ka->k, kaa->K, typed 'ka', and now typing 'a':
210     //   history.previous == 'k',1
211     //   history.current == 'k',1
212     //   history.ambi == 'a'
213     // So now we should get transform of 'k\u001d' + 'aa'.
214     result = parsedLayout.transform(
215         history.previous.text, history.previous.transat,
216         history.ambi + chars);
217     // Note: result.back could be negative number. In such case, we should give
218     // up the transform result. This is to be compatible the old vk behaviors.
219     if (result && result.back < 0) {
220       result = null;
221     }
222   }
223   if (result) {
224     // Because the result is related to previous history, adjust the result so
225     // that it is related to current history.
226     var prev = history.previous.text;
227     prev = prev.slice(0, prev.length - result.back);
228     prev += result.chars;
229     result.back = history.current.text.length;
230     result.chars = prev;
231   } else {
232     // If no ambi chars or no transforms for ambi chars, try to match the
233     // regular transforms. In above case, if now typing 'b', we should get
234     // transform of 'k\u001d' + 'b'.
235     result = parsedLayout.transform(
236         history.current.text, history.current.transat, chars);
237   }
238   // Updates the history state.
239   if (parsedLayout.isAmbiChars(history.ambi + chars)) {
240     if (!history.ambi) {
241       // Empty ambi means chars should be the first ambi chars.
242       // So now we should set the previous.
243       history.previous = goog.object.clone(history.current);
244     }
245     history.ambi += chars;
246   } else if (parsedLayout.isAmbiChars(chars)) {
247     // chars could match ambi regex when ambi+chars cannot.
248     // In this case, record the current history to previous, and set ambi as
249     // chars.
250     history.previous = goog.object.clone(history.current);
251     history.ambi = chars;
252   } else {
253     history.previous.text = '';
254     history.previous.transat = -1;
255     history.ambi = '';
256   }
257   // Updates the history text per transform result.
258   var text = history.current.text;
259   var transat = history.current.transat;
260   if (result) {
261     text = text.slice(0, text.length - result.back);
262     text += result.chars;
263     transat = text.length;
264   } else {
265     text += chars;
266     // This function doesn't return null. So if result is null, fill it.
267     result = {back: 0, chars: chars};
268   }
269   // The history text cannot cannot contain SPACE!
270   var spacePos = text.lastIndexOf(' ');
271   if (spacePos >= 0) {
272     text = text.slice(spacePos + 1);
273     if (transat > spacePos) {
274       transat -= spacePos + 1;
275     } else {
276       transat = -1;
277     }
278   }
279   history.current.text = text;
280   history.current.transat = transat;
282   return result;
287  * Wether the active layout has transforms defined.
289  * @return {boolean} True if transforms defined, false otherwise.
290  */
291 i18n.input.chrome.vk.Model.prototype.hasTransforms = function() {
292   var parsedLayout = this.layouts_[this.activeLayout_];
293   return !!parsedLayout && !!parsedLayout.transforms;
298  * Processes the backspace key. It affects the history state.
300  * @param {string} charsBeforeCaret The chars before the caret in the active
301  *     input box. This will be used to compare with the history states.
302  */
303 i18n.input.chrome.vk.Model.prototype.processBackspace = function(
304     charsBeforeCaret) {
305   this.matchHistory_(charsBeforeCaret);
307   var history = this.historyState_;
308   // Reverts the current history. If the backspace across over the transat pos,
309   // clean it up.
310   var text = history.current.text;
311   if (text) {
312     text = text.slice(0, text.length - 1);
313     history.current.text = text;
314     if (history.current.transat > text.length) {
315       history.current.transat = text.length;
316     }
318     text = history.ambi;
319     if (text) { // If there is ambi text, remove the last char in ambi.
320       history.ambi = text.slice(0, text.length - 1);
321     }
322     // Prev history only exists when ambi is not empty.
323     if (!history.ambi) {
324       history.previous = {text: '', transat: -1};
325     }
326   } else {
327     // Cleans up the previous history.
328     history.previous = {text: '', transat: -1};
329     history.ambi = '';
330     // Cleans up the current history.
331     history.current = goog.object.clone(history.previous);
332   }
337  * Callback when layout loaded.
339  * @param {!Object} layout The layout object passed from the layout JS's loadme
340  *     callback.
341  * @private
342  */
343 i18n.input.chrome.vk.Model.prototype.onLayoutLoaded_ = function(layout) {
344   var parsedLayout = new i18n.input.chrome.vk.ParsedLayout(layout);
345   if (parsedLayout.id) {
346     this.layouts_[parsedLayout.id] = parsedLayout;
347   }
348   if (this.delayActiveLayout_ == layout.id) {
349     this.activateLayout(this.delayActiveLayout_);
350     this.delayActiveLayout_ = '';
351   }
352   this.dispatchEvent(new i18n.input.chrome.vk.LayoutEvent(
353       i18n.input.chrome.vk.EventType.LAYOUT_LOADED, parsedLayout));
358  * Matches the given text to the last transformed text. Clears history if they
359  * are not matched.
361  * @param {string} text The text to be matched.
362  * @private
363  */
364 i18n.input.chrome.vk.Model.prototype.matchHistory_ = function(text) {
365   var hisText = this.historyState_.current.text;
366   if (!hisText || !text || !(goog.string.endsWith(text, hisText) ||
367       goog.string.endsWith(hisText, text))) {
368     this.clearHistory();
369   }
374  * Clears the history state.
375  */
376 i18n.input.chrome.vk.Model.prototype.clearHistory = function() {
377   this.historyState_.ambi = '';
378   this.historyState_.previous = {text: '', transat: -1};
379   this.historyState_.current = goog.object.clone(this.historyState_.previous);
384  * Prunes the history state to remove a number of chars at beginning.
386  * @param {number} count The count of chars to be removed.
387  */
388 i18n.input.chrome.vk.Model.prototype.pruneHistory = function(count) {
389   var pruneFunc = function(his) {
390     his.text = his.text.slice(count);
391     if (his.transat > 0) {
392       his.transat -= count;
393       if (his.transat <= 0) {
394         his.transat = -1;
395       }
396     }
397   };
398   pruneFunc(this.historyState_.previous);
399   pruneFunc(this.historyState_.current);
404  * Loads the script for a layout.
406  * @param {string} layoutCode The layout code.
407  * @private
408  */
409 i18n.input.chrome.vk.Model.loadLayoutScript_ = function(layoutCode) {
410   goog.net.jsloader.load('layouts/' + layoutCode + '.js');