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