OLD | NEW |
(Empty) | |
| 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 }; |
OLD | NEW |