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
8 // http://www.apache.org/licenses/LICENSE-2.0
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");
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.
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');
37 * Creates the Model object.
40 * @extends {goog.events.EventTarget}
42 i18n
.input
.chrome
.vk
.Model = function() {
46 * The registered layouts object.
47 * Its format is {<layout code>: <parsed layout obj>}.
49 * @type {!Object.<!i18n.input.chrome.vk.ParsedLayout|boolean>}
55 * The active layout code.
60 this.activeLayout_
= '';
63 * The layout code of which the layout is "being activated" when the layout
64 * hasn't been loaded yet.
69 this.delayActiveLayout_
= '';
72 * History state used for ambiguous transforms.
77 this.historyState_
= {
78 previous
: {text
: '', transat
: -1},
80 current
: {text
: '', transat
: -1}
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
);
90 * Loads the layout in the background.
92 * @param {string} layoutCode The layout will be loaded.
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
)));
111 * Activate layout by setting the current layout.
113 * @param {string} layoutCode The layout will be set as current layout.
115 i18n
.input
.chrome
.vk
.Model
.prototype.activateLayout = function(
117 if (!layoutCode
) return;
119 if (this.activeLayout_
!= layoutCode
) {
120 var parsedLayout
= this.layouts_
[layoutCode
];
122 this.activeLayout_
= layoutCode
;
123 this.delayActiveLayout_
= '';
125 } else if (parsedLayout
== false) { // Layout being loaded?
126 this.delayActiveLayout_
= layoutCode
;
133 * Gets the current layout.
135 * @return {string} The current layout code.
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.
147 i18n
.input
.chrome
.vk
.Model
.prototype.predictHistory = function() {
148 if (!this.activeLayout_
|| !this.layouts_
[this.activeLayout_
]) {
151 var parsedLayout
= this.layouts_
[this.activeLayout_
];
152 var history
= this.historyState_
;
155 text
= history
.previous
.text
;
156 transat
= history
.previous
.transat
;
157 // Tries to predict transform for previous history.
159 text
= text
.slice(0, transat
) + '\u001d' + text
.slice(transat
) +
162 text
+= history
.ambi
;
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.
170 // Tries to predict transform for current history.
171 text
= history
.current
.text
;
172 transat
= history
.current
.transat
;
174 text
= text
.slice(0, transat
) + '\u001d' + text
.slice(transat
);
176 var pos
= parsedLayout
.predictTransform(text
);
177 if (transat
>= 0 && pos
> transat
) {
178 // Adjusts the pos for removing the temporary \u001d character.
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.
195 i18n
.input
.chrome
.vk
.Model
.prototype.translate = function(
196 chars
, charsBeforeCaret
) {
197 if (!this.activeLayout_
|| !chars
) {
200 var parsedLayout
= this.layouts_
[this.activeLayout_
];
205 this.matchHistory_(charsBeforeCaret
);
206 var result
, history
= this.historyState_
;
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) {
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
;
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
);
238 // Updates the history state.
239 if (parsedLayout
.isAmbiChars(history
.ambi
+ chars
)) {
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
);
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
250 history
.previous
= goog
.object
.clone(history
.current
);
251 history
.ambi
= chars
;
253 history
.previous
.text
= '';
254 history
.previous
.transat
= -1;
257 // Updates the history text per transform result.
258 var text
= history
.current
.text
;
259 var transat
= history
.current
.transat
;
261 text
= text
.slice(0, text
.length
- result
.back
);
262 text
+= result
.chars
;
263 transat
= text
.length
;
266 // This function doesn't return null. So if result is null, fill it.
267 result
= {back
: 0, chars
: chars
};
269 // The history text cannot cannot contain SPACE!
270 var spacePos
= text
.lastIndexOf(' ');
272 text
= text
.slice(spacePos
+ 1);
273 if (transat
> spacePos
) {
274 transat
-= spacePos
+ 1;
279 history
.current
.text
= text
;
280 history
.current
.transat
= transat
;
287 * Wether the active layout has transforms defined.
289 * @return {boolean} True if transforms defined, false otherwise.
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.
303 i18n
.input
.chrome
.vk
.Model
.prototype.processBackspace = function(
305 this.matchHistory_(charsBeforeCaret
);
307 var history
= this.historyState_
;
308 // Reverts the current history. If the backspace across over the transat pos,
310 var text
= history
.current
.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
;
319 if (text
) { // If there is ambi text, remove the last char in ambi.
320 history
.ambi
= text
.slice(0, text
.length
- 1);
322 // Prev history only exists when ambi is not empty.
324 history
.previous
= {text
: '', transat
: -1};
327 // Cleans up the previous history.
328 history
.previous
= {text
: '', transat
: -1};
330 // Cleans up the current history.
331 history
.current
= goog
.object
.clone(history
.previous
);
337 * Callback when layout loaded.
339 * @param {!Object} layout The layout object passed from the layout JS's loadme
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
;
348 if (this.delayActiveLayout_
== layout
.id
) {
349 this.activateLayout(this.delayActiveLayout_
);
350 this.delayActiveLayout_
= '';
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
361 * @param {string} text The text to be matched.
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
))) {
374 * Clears the history state.
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.
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) {
398 pruneFunc(this.historyState_
.previous
);
399 pruneFunc(this.historyState_
.current
);
404 * Loads the script for a layout.
406 * @param {string} layoutCode The layout code.
409 i18n
.input
.chrome
.vk
.Model
.loadLayoutScript_ = function(layoutCode
) {
410 goog
.net
.jsloader
.load('layouts/' + layoutCode
+ '.js');