| // Copyright 2010-2015, Google Inc. |
| // All rights reserved. |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are |
| // met: |
| // |
| // * Redistributions of source code must retain the above copyright |
| // notice, this list of conditions and the following disclaimer. |
| // * Redistributions in binary form must reproduce the above |
| // copyright notice, this list of conditions and the following disclaimer |
| // in the documentation and/or other materials provided with the |
| // distribution. |
| // * Neither the name of Google Inc. nor the names of its |
| // contributors may be used to endorse or promote products derived from |
| // this software without specific prior written permission. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| /** |
| * @fileoverview Sample IME for ChromeOS with IME Extension API. |
| */ |
| |
| 'use strict'; |
| |
| /** |
| * Namespace for this extension. |
| */ |
| var sampleImeForImeExtensionApi = window.sampleImeForImeExtensionApi || {}; |
| |
| /** |
| * Sample IME with IME extension API. |
| * @constructor |
| */ |
| sampleImeForImeExtensionApi.SampleIme = function() { |
| /** |
| * Context information which is provided by Chrome. |
| * @type {Object} |
| * @private |
| */ |
| this.context_ = null; |
| |
| /** |
| * Engine id which is specified on manifest.js. |
| * @type {string} |
| * @private |
| */ |
| this.engineID_ = ''; |
| |
| // Some properties are initialized on |
| // {@code sampleImeForImeExtensionApi.SampleIme.clear_} and |
| // {@code sampleImeForImeExtensionApi.SampleIme.initializeMenuItems_}. |
| this.clear_(); |
| this.initializeMenuItems_(); |
| |
| var ime = this; |
| chrome.input.ime.onActivate.addListener( |
| function(engineID) { ime.onActivate(engineID); }); |
| chrome.input.ime.onDeactivated.addListener( |
| function(engineID) { ime.onDeactivated(engineID); }); |
| chrome.input.ime.onFocus.addListener( |
| function(context) { ime.onFocus(context); }); |
| chrome.input.ime.onBlur.addListener( |
| function(contextID) { ime.onBlur(contextID); }); |
| chrome.input.ime.onInputContextUpdate.addListener( |
| function(context) { ime.onInputContextUpdate(context); }); |
| chrome.input.ime.onKeyEvent.addListener( |
| function(engineID, keyData) { |
| return ime.onKeyEvent(engineID, keyData); |
| }); |
| chrome.input.ime.onCandidateClicked.addListener( |
| function(engineID, candidateID, button) { |
| ime.onCandidateClicked(engineID, candidateID, button); |
| }); |
| chrome.input.ime.onMenuItemActivated.addListener( |
| function(engineID, name) { |
| ime.onMenuItemActivated(engineID, name); |
| }); |
| }; |
| |
| /** |
| * Stringifies key event data. |
| * @param {!Object} keyData Key event data. |
| * @return {string} Stringified key event data. |
| * @private |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.stringifyKeyAndModifiers_ = |
| function(keyData) { |
| var keys = [keyData.key]; |
| if (keyData.altKey) { keys.push('alt'); } |
| if (keyData.ctrlKey) { keys.push('ctrl'); } |
| if (keyData.shiftKey) { keys.push('shift'); } |
| return keys.join(' '); |
| }; |
| |
| /** |
| * Ignorable key set to determine we handle the key event or not. |
| * @type {!Object.<boolean>} |
| * @private |
| */ |
| sampleImeForImeExtensionApi.SampleIme.IGNORABLE_KEY_SET_ = (function() { |
| var IGNORABLE_KEYS = [ |
| { // PrintScreen shortcut. |
| key: 'ChromeOSSwitchWindow', |
| ctrlKey: true |
| }, { // PrintScreen shortcut. |
| key: 'ChromeOSSwitchWindow', |
| ctrlKey: true, |
| shiftKey: true |
| } |
| ]; |
| |
| var ignorableKeySet = []; |
| for (var i = 0; i < IGNORABLE_KEYS.length; ++i) { |
| var key = sampleImeForImeExtensionApi.SampleIme.prototype. |
| stringifyKeyAndModifiers_(IGNORABLE_KEYS[i]); |
| ignorableKeySet[key] = true; |
| } |
| |
| return ignorableKeySet; |
| })(); |
| |
| /** |
| * Immutable conversion table. It is used to suggest special candidates. |
| * @type {!Object.<string>} |
| * @private |
| */ |
| sampleImeForImeExtensionApi.SampleIme.CONVERSION_TABLE_ = { |
| star: '\u2606', // '☆' |
| heart: '\u2661' // '♡' |
| }; |
| |
| /** |
| * Page size of a candidate list. |
| * This value should not be greater than 12 since we use Roman number to |
| * indicates the candidate number on the list. |
| * @type {number} |
| * @private |
| */ |
| sampleImeForImeExtensionApi.SampleIme.PAGE_SIZE_ = 5; |
| |
| /** |
| * Enum of IME state. |
| * @enum {number} |
| */ |
| sampleImeForImeExtensionApi.SampleIme.State = { |
| /** IME doesn't have any input text. */ |
| PRECOMPOSITION: 0, |
| /** |
| * IME has a input text, but no candidate are expressly selected or input text |
| * is not segmented. |
| */ |
| COMPOSITION: 1, |
| /** |
| * IME has a input text, and one of the candidate is selected or input text is |
| * segmentated. |
| */ |
| CONVERSION: 2 |
| }; |
| |
| /** |
| * Segment information of a composition text. |
| * @constructor |
| */ |
| sampleImeForImeExtensionApi.SampleIme.Segment = function() { |
| /** |
| * Start position of the segment. |
| * @type {number} |
| */ |
| this.start = 0; |
| |
| /** |
| * Candidates list. |
| * @type {!Array.<string>} |
| */ |
| this.candidates = []; |
| |
| /** |
| * Focused candidate index. |
| * @type {number} |
| */ |
| this.focusedIndex = 0; |
| }; |
| |
| /** |
| * Initializes menu items and some member variables. |
| * @private |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.initializeMenuItems_ = |
| function() { |
| var menuItems = []; |
| var callbacks = {}; |
| var that = this; |
| |
| /** |
| * Indicates IME suggests dummy candidates or not. |
| * @type {boolean} |
| * @private |
| */ |
| this.useDummyCandidates_ = true; |
| |
| var radioItems = [{ |
| id: 'enable_dummy_candidates', |
| label: 'Enable dummy candidates', |
| style: 'radio', |
| checked: true, |
| enabled: true, |
| visible: true |
| }, { |
| id: 'disable_dummy_candidates', |
| label: 'Disable dummy candidates', |
| style: 'radio', |
| checked: false, |
| enabled: true, |
| visible: true |
| }]; |
| for (var i = 0; i < radioItems.length; ++i) { |
| callbacks[radioItems[i].id] = (function(index) { |
| return function() { |
| for (var j = 0; j < radioItems.length; ++j) { |
| var isChecked = (index == j); |
| radioItems[j].checked = isChecked; |
| } |
| var isEnabled = index == 0; |
| that.useDummyCandidates_ = isEnabled; |
| }; |
| })(i); |
| menuItems.push(radioItems[i]); |
| } |
| |
| var enableRadioMenuItem = { |
| id: 'enable_radio_menu', |
| label: 'Enable radio menu', |
| style: 'check', |
| checked: true |
| }; |
| menuItems.push(enableRadioMenuItem); |
| callbacks[enableRadioMenuItem.id] = function() { |
| enableRadioMenuItem.checked = !enableRadioMenuItem.checked; |
| for (var i = 0; i < radioItems.length; ++i) { |
| radioItems[i].enabled = enableRadioMenuItem.checked; |
| } |
| }; |
| |
| var displayRadioMenuItem = { |
| id: 'display_radio_menu', |
| label: 'Display radio menu', |
| style: 'check', |
| checked: true |
| }; |
| menuItems.push(displayRadioMenuItem); |
| callbacks[displayRadioMenuItem.id] = function() { |
| displayRadioMenuItem.checked = !displayRadioMenuItem.checked; |
| for (var i = 0; i < radioItems.length; ++i) { |
| radioItems[i].visible = displayRadioMenuItem.checked; |
| } |
| }; |
| |
| /** |
| * Menu items of this IME. |
| */ |
| this.menuItems_ = menuItems; |
| |
| /** |
| * Callback function table which is called when menu item is clicked. |
| */ |
| this.menuItemCallbackTable_ = callbacks; |
| }; |
| |
| /** |
| * Clears properties of IME. |
| * @private |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.clear_ = function() { |
| /** |
| * Raw input text. |
| * @type {string} |
| * @private |
| */ |
| this.inputText_ = ''; |
| |
| /** |
| * Commit text. |
| * This is a volatile property, and will be cleared by |
| * {@code sampleImeForImeExtensionApi.SampleIme.updateCommitText_}. |
| * @type {?string} |
| * @private |
| */ |
| this.commitText_ = null; |
| |
| /** |
| * Segments information. |
| * @type {!Array.<!sampleImeForImeExtensionApi.SampleIme.Segment>} |
| * @private |
| */ |
| this.segments_ = []; |
| |
| /** |
| * Cursor position. |
| * @type {number} |
| * @private |
| */ |
| this.cursor_ = 0; |
| |
| /** |
| * Focused segment index. |
| * @type {number} |
| * @private |
| */ |
| this.focusedSegmentIndex_ = 0; |
| |
| /** |
| * The state of the IME. |
| * @type {sampleImeForImeExtensionApi.SampleIme.State} |
| * @private |
| */ |
| this.state_ = sampleImeForImeExtensionApi.SampleIme.State.PRECOMPOSITION; |
| }; |
| |
| /** |
| * Determines that IME is enabled or not using a context information. |
| * @return {boolean} IME is enabled or not. |
| * @private |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.isImeEnabled_ = function() { |
| return this.context_ != null; |
| }; |
| |
| /** |
| * Appends a new empty segment on |
| * {@code sampleImeForImeExtensionApi.SampleIme.segments}. |
| * @private |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.appendNewSegment_ = function() { |
| var startPosition = this.inputText_.length; |
| if (this.segments_.length == 0) { |
| startPosition = 0; |
| } |
| |
| var newSegment = new sampleImeForImeExtensionApi.SampleIme.Segment(); |
| newSegment.start = startPosition; |
| |
| this.segments_.push(newSegment); |
| }; |
| |
| /** |
| * Gets input text on the segment. |
| * @param {number=} opt_segmentIndex Index of the segment you want to get |
| * a text. this.focusedSegmentIndex_ is used as a default value. |
| * @return {string} Input text of the segment. |
| * @private |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.getInputTextOnSegment_ = |
| function(opt_segmentIndex) { |
| if (this.state_ == |
| sampleImeForImeExtensionApi.SampleIme.State.PRECOMPOSITION) { |
| return ''; |
| } |
| |
| var segmentIndex = (opt_segmentIndex == undefined) ? |
| this.focusedSegmentIndex_ : opt_segmentIndex; |
| if (segmentIndex < 0 || this.segments_.length <= segmentIndex) { |
| return ''; |
| } |
| |
| var start = this.segments_[segmentIndex].start; |
| var end = (segmentIndex + 1 == this.segments_.length) ? |
| this.inputText_.length : this.segments_[segmentIndex + 1].start; |
| var length = end - start; |
| |
| return this.inputText_.substr(start, length); |
| }; |
| |
| /** |
| * Generates and sets candidates of the segment. |
| * @param {number=} opt_segmentIndex Index of the segment you want to get a |
| * text. {@code sampleImeForImeExtensionApi.SampleIme.focusedSegmentIndex_} |
| * is used as a default value. |
| * @private |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.generateCandidates_ = |
| function(opt_segmentIndex) { |
| if (this.state_ == |
| sampleImeForImeExtensionApi.SampleIme.State.PRECOMPOSITION) { |
| return; |
| } |
| |
| var segmentIndex = (opt_segmentIndex == undefined) ? |
| this.focusedSegmentIndex_ : opt_segmentIndex; |
| if (segmentIndex < 0 || this.segments_.length <= segmentIndex) { |
| return; |
| } |
| |
| var segment = this.segments_[segmentIndex]; |
| var text = this.getInputTextOnSegment_(segmentIndex); |
| |
| segment.focusedIndex = 0; |
| |
| if (text == '') { |
| segment.candidates = []; |
| return; |
| } |
| |
| segment.candidates = [ |
| text.replace(/(\w)/g, function(match) { |
| // Converts ASCII alphabet characters to its fullwidth versions. |
| var offset = 65248; |
| return String.fromCharCode(match.charCodeAt(0) + offset); |
| }), |
| text, |
| text.toUpperCase(), |
| text.substr(0, 1).toUpperCase() + text.substr(1).toLowerCase() |
| ]; |
| |
| var table = sampleImeForImeExtensionApi.SampleIme.CONVERSION_TABLE_; |
| if (text in table) { |
| segment.candidates.push(table[text]); |
| } |
| |
| if (this.useDummyCandidates_) { |
| segment.candidates.push('DummyCandidate1'); |
| segment.candidates.push('DummyCandidate2'); |
| } |
| }; |
| |
| /** |
| * Gets preedit text. |
| * @return {string} Preedit text. |
| * @private |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.getPreeditText_ = function() { |
| if (this.state_ == |
| sampleImeForImeExtensionApi.SampleIme.State.PRECOMPOSITION) { |
| return ''; |
| } |
| |
| var texts = []; |
| for (var i = 0; i < this.segments_.length; ++i) { |
| var segment = this.segments_[i]; |
| texts.push(segment.candidates[segment.focusedIndex]); |
| } |
| return texts.join(''); |
| }; |
| |
| /** |
| * Updates preedit text. |
| * @private |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.updatePreedit_ = function() { |
| if (this.state_ == |
| sampleImeForImeExtensionApi.SampleIme.State.PRECOMPOSITION) { |
| chrome.input.ime.clearComposition({ |
| contextID: this.context_.contextID |
| }, function(success) { |
| console.log('Composition is cleared. result=' + success); |
| }); |
| return; |
| } |
| |
| var segmentsData = []; |
| for (var i = 0; i < this.segments_.length; ++i) { |
| var text = this.segments_[i].candidates[this.segments_[i].focusedIndex]; |
| var start = i == 0 ? 0 : segmentsData[i - 1].end; |
| var end = start + text.length; |
| |
| segmentsData.push({ |
| start: start, |
| end: end, |
| style: 'underline' |
| }); |
| } |
| if (this.state_ == sampleImeForImeExtensionApi.SampleIme.State.CONVERSION) { |
| segmentsData[this.focusedSegmentIndex_].style = 'doubleUnderline'; |
| } |
| |
| var cursorPos = 0; |
| if (this.state_ == sampleImeForImeExtensionApi.SampleIme.State.CONVERSION) { |
| for (var i = 0; i < this.focusedSegmentIndex_; ++i) { |
| var segment = this.segments_[i]; |
| cursorPos += segment.candidates[segment.focusedIndex].length; |
| } |
| } else { |
| cursorPos = this.cursor_; |
| } |
| |
| var composition = { |
| contextID: this.context_.contextID, |
| text: this.getPreeditText_(), |
| segments: segmentsData, |
| cursor: cursorPos |
| }; |
| |
| if (this.state_ == sampleImeForImeExtensionApi.SampleIme.State.CONVERSION) { |
| composition.selectionStart = segmentsData[this.focusedSegmentIndex_].start; |
| composition.selectionEnd = segmentsData[this.focusedSegmentIndex_].end; |
| } |
| |
| chrome.input.ime.setComposition(composition, function(success) { |
| console.log('Composition is set. result=' + success); |
| }); |
| }; |
| |
| /** |
| * Updates candidates. |
| * @private |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.updateCandidates_ = function() { |
| var candidateWindowPropertiesCallback = function(success) { |
| console.log('Candidate window properties are updated. result=' + success); |
| }; |
| |
| if (this.state_ == |
| sampleImeForImeExtensionApi.SampleIme.State.PRECOMPOSITION) { |
| chrome.input.ime.setCandidateWindowProperties({ |
| engineID: this.engineID_, |
| properties: { |
| visible: false, |
| auxiliaryTextVisible: false |
| } |
| }, candidateWindowPropertiesCallback); |
| } else { |
| var PAGE_SIZE = sampleImeForImeExtensionApi.SampleIme.PAGE_SIZE_; |
| |
| var segment = this.segments_[this.focusedSegmentIndex_]; |
| var labels = []; |
| for (var i = 0; i < PAGE_SIZE; ++i) { |
| // Roman number. |
| labels.push(String.fromCharCode(0x2160 + i)); // 'Ⅰ' + i |
| } |
| |
| chrome.input.ime.setCandidates({ |
| contextID: this.context_.contextID, |
| candidates: segment.candidates.map(function(value, index) { |
| var candidate = { |
| candidate: value, |
| id: index, |
| label: labels[index % PAGE_SIZE] |
| }; |
| if (index == 0) { |
| candidate.annotation = '1st candidate'; |
| } |
| return candidate; |
| }) |
| }, function(success) { |
| console.log('Candidates are set. result=' + success); |
| }); |
| chrome.input.ime.setCursorPosition({ |
| contextID: this.context_.contextID, |
| candidateID: segment.focusedIndex |
| }, function(success) { |
| console.log('Cursor position is set. result=' + success); |
| }); |
| chrome.input.ime.setCandidateWindowProperties({ |
| engineID: this.engineID_, |
| properties: { |
| visible: true, |
| cursorVisible: true, |
| vertical: true, |
| pageSize: PAGE_SIZE, |
| auxiliaryTextVisible: true, |
| auxiliaryText: 'Sample IME' |
| } |
| }, candidateWindowPropertiesCallback); |
| } |
| }; |
| |
| /** |
| * Updates commit text if {@code commitText_} isn't null. |
| * This function clears {@code commitText_} since it is a volatile property. |
| * @private |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.updateCommitText_ = function() { |
| if (this.commitText_ === null) { |
| return; |
| } |
| |
| chrome.input.ime.commitText({ |
| contextID: this.context_.contextID, |
| text: this.commitText_ |
| }, function(success) { |
| console.log('Commited. result=' + success); |
| }); |
| |
| this.commitText_ = null; |
| }; |
| |
| /** |
| * Updates output using IME Extension API. |
| * @private |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.update_ = function() { |
| this.updatePreedit_(); |
| this.updateCandidates_(); |
| this.updateCommitText_(); |
| }; |
| |
| /** |
| * Commits a preedit text and clears a context. |
| * @private |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.commit_ = function() { |
| if (this.state_ == |
| sampleImeForImeExtensionApi.SampleIme.State.PRECOMPOSITION) { |
| return; |
| } |
| |
| var commitText = this.getPreeditText_(); |
| this.clear_(); |
| this.commitText_ = commitText; |
| }; |
| |
| /** |
| * Inserts characters into the cursor position. |
| * @param {string} value Text we want to insert into. |
| * @private |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.insertCharacters_ = |
| function(value) { |
| if (this.state_ == sampleImeForImeExtensionApi.SampleIme.State.CONVERSION) { |
| return; |
| } |
| |
| if (this.state_ == |
| sampleImeForImeExtensionApi.SampleIme.State.PRECOMPOSITION) { |
| this.appendNewSegment_(); |
| this.focusedSegmentIndex_ = 0; |
| } |
| |
| var text = this.inputText_; |
| this.inputText_ = |
| text.substr(0, this.cursor_) + value + text.substr(this.cursor_); |
| this.state_ = sampleImeForImeExtensionApi.SampleIme.State.COMPOSITION; |
| this.moveCursor_(this.cursor_ + value.length); |
| |
| this.generateCandidates_(); |
| }; |
| |
| /** |
| * Removes a character. |
| * @param {number} index index of the character you want to remove. |
| * @private |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.removeCharacter_ = |
| function(index) { |
| if (this.state_ != sampleImeForImeExtensionApi.SampleIme.State.COMPOSITION) { |
| return; |
| } |
| |
| if (index < 0 || this.inputText_.length <= index) { |
| return; |
| } |
| |
| this.inputText_ = |
| this.inputText_.substr(0, index) + this.inputText_.substr(index + 1); |
| |
| if (this.inputText_.length == 0) { |
| this.clear_(); |
| return; |
| } |
| |
| if (index < this.cursor_) { |
| this.moveCursor_(this.cursor_ - 1); |
| } |
| |
| this.generateCandidates_(); |
| }; |
| |
| /** |
| * Moves a cursor position. |
| * @param {number} index Cursor position. |
| * @private |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.moveCursor_ = function(index) { |
| if (this.state_ != sampleImeForImeExtensionApi.SampleIme.State.COMPOSITION) { |
| return; |
| } |
| |
| if (index < 0 || this.inputText_.length < index) { |
| return; |
| } |
| |
| this.cursor_ = index; |
| }; |
| |
| /** |
| * Expands a focused segment. |
| * @private |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.expandSegment_ = function() { |
| if (this.state_ == |
| sampleImeForImeExtensionApi.SampleIme.State.PRECOMPOSITION) { |
| return; |
| } |
| |
| this.state_ = sampleImeForImeExtensionApi.SampleIme.State.CONVERSION; |
| |
| var index = this.focusedSegmentIndex_; |
| var segments = this.segments_; |
| if (index + 1 >= segments.length) { |
| return; |
| } |
| |
| if ((index + 2 == segments.length && |
| segments[index + 1].start + 1 == this.inputText_.length) || |
| (index + 2 < segments.length && |
| segments[index + 1].start + 1 == segments[index + 2].start)) { |
| segments.splice(index + 1, 1); |
| } else { |
| ++segments[index + 1].start; |
| this.generateCandidates_(index + 1); |
| } |
| |
| this.generateCandidates_(); |
| }; |
| |
| /** |
| * Shrinks a focused segment. |
| * @private |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.shrinkSegment_ = function() { |
| if (this.state_ == |
| sampleImeForImeExtensionApi.SampleIme.State.PRECOMPOSITION) { |
| return; |
| } |
| |
| this.state_ = sampleImeForImeExtensionApi.SampleIme.State.CONVERSION; |
| |
| var segments = this.segments_; |
| var index = this.focusedSegmentIndex_; |
| |
| if (index + 1 == segments.length) { |
| if (this.inputText_.length - segments[index].start > 1) { |
| this.appendNewSegment_(); |
| segments[index + 1].start = this.inputText_.length - 1; |
| this.generateCandidates_(); |
| this.generateCandidates_(index + 1); |
| } |
| } else { |
| if (segments[index + 1].start - segments[index].start > 1) { |
| --segments[index + 1].start; |
| this.generateCandidates_(); |
| this.generateCandidates_(index + 1); |
| } |
| } |
| }; |
| |
| /** |
| * Resets a segmentation data of the preedit text. |
| * @private |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.resetSegments_ = function() { |
| if (this.state_ != sampleImeForImeExtensionApi.SampleIme.State.CONVERSION) { |
| return; |
| } |
| |
| this.segments_ = []; |
| this.appendNewSegment_(); |
| this.focusedSegmentIndex_ = 0; |
| this.generateCandidates_(); |
| this.state_ = sampleImeForImeExtensionApi.SampleIme.State.COMPOSITION; |
| }; |
| |
| /** |
| * Selects a candidate. |
| * @param {number} candidateID index of the candidate. |
| * @private |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.focusCandidate_ = |
| function(candidateID) { |
| if (this.state_ == |
| sampleImeForImeExtensionApi.SampleIme.State.PRECOMPOSITION) { |
| return; |
| } |
| |
| var segment = this.segments_[this.focusedSegmentIndex_]; |
| if (candidateID < 0 || segment.candidates.length <= candidateID) { |
| return; |
| } |
| |
| segment.focusedIndex = candidateID; |
| this.state_ = sampleImeForImeExtensionApi.SampleIme.State.CONVERSION; |
| }; |
| |
| /** |
| * Focuses a segment. |
| * @param {number} segmentID index of the segment. |
| * @private |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.focusSegment_ = |
| function(segmentID) { |
| if (this.state_ != sampleImeForImeExtensionApi.SampleIme.State.CONVERSION) { |
| return; |
| } |
| |
| if (segmentID < 0 || this.segments_.length <= segmentID) { |
| return; |
| } |
| |
| this.focusedSegmentIndex_ = segmentID; |
| }; |
| |
| /** |
| * Handles a alphabet key. |
| * @param {!Object} keyData key event data. |
| * @return {boolean} true if key event is consumed. |
| * @private |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.handleKey_ = function(keyData) { |
| var keyValue = keyData.key; |
| |
| if (keyData.altKey || keyData.ctrlKey || keyData.shiftKey) { |
| return false; |
| } |
| |
| if (!keyValue.match(/^[a-z]$/i)) { |
| return false; |
| } |
| |
| if (this.state_ == sampleImeForImeExtensionApi.SampleIme.State.CONVERSION) { |
| this.commit_(); |
| } |
| |
| this.insertCharacters_(keyValue); |
| |
| this.update_(); |
| |
| return true; |
| }; |
| |
| /** |
| * Handles a non-alphabet key. |
| * @param {!Object} keyData key event data. |
| * @return {boolean} true if key event is consumed. |
| * @private |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.handleSpecialKey_ = |
| function(keyData) { |
| if (this.state_ == |
| sampleImeForImeExtensionApi.SampleIme.State.PRECOMPOSITION) { |
| return false; |
| } |
| |
| var segment = this.segments_[this.focusedSegmentIndex_]; |
| |
| if (!keyData.altKey && !keyData.ctrlKey && !keyData.shiftKey) { |
| switch (keyData.key) { |
| case 'Backspace': |
| if (this.state_ == |
| sampleImeForImeExtensionApi.SampleIme.State.CONVERSION) { |
| this.resetSegments_(); |
| } else if (this.cursor_ != 0) { |
| this.removeCharacter_(this.cursor_ - 1); |
| } |
| break; |
| case 'Delete': |
| if (this.state_ == |
| sampleImeForImeExtensionApi.SampleIme.State.CONVERSION) { |
| this.resetSegments_(); |
| } else if (this.cursor_ != this.inputText_.length) { |
| this.removeCharacter_(this.cursor_); |
| } |
| break; |
| case 'Up': |
| var previous_index = segment.focusedIndex - 1; |
| if (previous_index == -1) { |
| previous_index = segment.candidates.length - 1; |
| } |
| this.focusCandidate_(previous_index); |
| break; |
| case 'Down': |
| case ' ': |
| var next_index = segment.focusedIndex + 1; |
| if (next_index == segment.candidates.length) { |
| next_index = 0; |
| } |
| this.focusCandidate_(next_index); |
| break; |
| case 'Left': |
| if (this.state_ == |
| sampleImeForImeExtensionApi.SampleIme.State.CONVERSION) { |
| if (this.focusedSegmentIndex_ != 0) { |
| this.focusSegment_(this.focusedSegmentIndex_ - 1); |
| } |
| } else { |
| this.moveCursor_(this.cursor_ - 1); |
| } |
| break; |
| case 'Right': |
| if (this.state_ == |
| sampleImeForImeExtensionApi.SampleIme.State.CONVERSION) { |
| if (this.focusedSegmentIndex_ + 1 != this.segments_.length) { |
| this.focusSegment_(this.focusedSegmentIndex_ + 1); |
| } |
| } else { |
| this.moveCursor_(this.cursor_ + 1); |
| } |
| break; |
| case 'Enter': |
| this.commit_(); |
| break; |
| default: |
| return true; |
| } |
| } else if (!keyData.altKey && !keyData.ctrlKey && keyData.shiftKey) { |
| switch (keyData.key) { |
| case 'Left': |
| if (this.state_ != |
| sampleImeForImeExtensionApi.SampleIme.State.PRECOMPOSITION) { |
| this.shrinkSegment_(); |
| } |
| break; |
| case 'Right': |
| if (this.state_ != |
| sampleImeForImeExtensionApi.SampleIme.State.PRECOMPOSITION) { |
| this.expandSegment_(); |
| } |
| break; |
| default: |
| return true; |
| } |
| } else { |
| return true; |
| } |
| |
| this.update_(); |
| return true; |
| }; |
| |
| /** |
| * Sets up a menu on a uber tray. |
| * @private |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.setUpMenu_ = function() { |
| chrome.input.ime.setMenuItems({ |
| engineID: this.engineID_, |
| items: this.menuItems_ |
| }, function() { |
| console.log('Menu items are set.'); |
| }); |
| }; |
| |
| /** |
| * Callback method. It is called when IME is activated. |
| * @param {string} engineID engine ID. |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.onActivate = |
| function(engineID) { |
| this.engineID_ = engineID; |
| this.clear_(); |
| this.setUpMenu_(); |
| }; |
| |
| /** |
| * Callback method. It is called when IME is deactivated. |
| * @param {string} engineID engine ID. |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.onDeactivated = |
| function(engineID) { |
| this.clear_(); |
| this.engineID_ = ''; |
| }; |
| |
| /** |
| * Callback method. It is called when a context acquires a focus. |
| * @param {!Object} context context information. |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.onFocus = function(context) { |
| this.context_ = context; |
| this.clear_(); |
| }; |
| |
| /** |
| * Callback method. It is called when a context lost a focus. |
| * @param {number} contextID ID of the context. |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.onBlur = function(contextID) { |
| this.clear_(); |
| this.context_ = null; |
| }; |
| |
| /** |
| * Callback method. It is called when properties of the context is changed. |
| * @param {!Object} context context information. |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.onInputContextUpdate = |
| function(context) { |
| this.context_ = context; |
| if (!this.isImeEnabled_()) { |
| this.clear_(); |
| } |
| |
| this.update_(); |
| }; |
| |
| /** |
| * Callback method. It is called when IME catches a new key event. |
| * @param {string} engineID ID of the engine. |
| * @param {!Object} keyData key event data. |
| * @return {boolean} true if the key event is consumed. |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.onKeyEvent = |
| function(engineID, keyData) { |
| if (keyData.type != 'keydown' || !this.isImeEnabled_()) { |
| return false; |
| } |
| |
| var key = this.stringifyKeyAndModifiers_(keyData); |
| if (sampleImeForImeExtensionApi.SampleIme.IGNORABLE_KEY_SET_[key]) { |
| return false; |
| } |
| |
| return this.handleKey_(keyData) || this.handleSpecialKey_(keyData); |
| }; |
| |
| /** |
| * Callback method. It is called when candidates on candidate window is clicked. |
| * @param {string} engineID ID of the engine. |
| * @param {number} candidateID Index of the candidate. |
| * @param {string} button Which mouse button was clicked. |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.onCandidateClicked = |
| function(engineID, candidateID, button) { |
| if (button == 'left') { |
| this.focusCandidate_(candidateID); |
| this.update_(); |
| } |
| }; |
| |
| /** |
| * Callback method. It is called when menu item on uber tray is activated. |
| * @param {string} engineID ID of the engine. |
| * @param {string} name name of the menu item. |
| */ |
| sampleImeForImeExtensionApi.SampleIme.prototype.onMenuItemActivated = |
| function(engineID, name) { |
| var callback = this.menuItemCallbackTable_[name]; |
| if (typeof(callback) != 'function') { |
| return; |
| } |
| |
| callback(); |
| |
| chrome.input.ime.updateMenuItems({ |
| engineID: engineID, |
| items: this.menuItems_ |
| }, function() { |
| console.log('Menu items are updated.'); |
| }); |
| }; |