Personal tools

WebAPI/KeboardIME

From MozillaWiki

Jump to: navigation, search

Contents


Introduction

Virtual Keyboard/IME API aims to implement the system IME as a Web App. It will be used in Firefox OS and use cases could be found in the Firefox OS Keyboard UX document(WIP).

The API provides the communication channel between the IME App and the other App that receives user's inputs.

It is very different from the IME API from Google that aims to re-use the system's IME in a web page.

Status

API discussion:

  1. WebAPI mailing list post
  2. Extended API mailing list post
  3. Polished Keyboard API

Implementation:

  1. bug 737110 - Bug 737110 - Virtual Keyboard API

Features

The Virtual Keyboard/IME API supports the following features:

  • Notifies the VKB app when the focus text field was changing in other

apps

  • Allow user to manual hide the keyboard. Check bug 737110.
  • The VKB app should be responsive to properties and the state of the input field (more than HTML5 input type, including current content, cursor position, x-inputmode bug 796544).
  • Sends trust
  • The VKB app should be able to send trusted key events such as they are considered by the other apps as user' inputs.
  • The VKB app should be able to send a character or a string to the current input cursor position.
  • Keyboard should be able to overwrite the current value of the input field of the input field and set the cursor position.
  • The VKB app should be able to move the cursor position or select a specified range of text.
  • The VKB should be able to switch the focus onto the previous/next input field.
  • The return key label of the VKB can be customized.

Proposed Manifest of a 3rd-Party IME

Just like any other apps, keyboard apps register themselves in the same apps registry. We extend the manifest syntax here to describe layout(s) available in a given keyboard app. Gaia will be paring the manifest. There are 3 special fields to distinguish and describe a 3rd-party IME:

  • [Line 4] a "role" field with value "keyboard" declares it's an IME app. Homescreen app will ignore some role types when displaying app icons, and "keyboard" is one of them. (see bug 892397)
  • [Line 6-8] a "permissions" field that requests "keyboard" permission. All IME apps need this permission for sending input keys and updating the value of a input field.
  • [Line 9-30] a "entry_points" field specifies supported layouts. Each layout is described in a key-value pair, where the key represents the layout name (will be shown up on Settings app with the app name), and the value describes the detailed information of the layout, including launch path of the layout and supported input types. (See #Layout Matching Algorithm)
    • The allowed value in "types" field is a subset of type attribute of input element: text, search, tel, number, url, email. Other types will be ignored by FxOS Gaia in the initial version because at this point UI for <select> and <input type=date> (called "value selectors") are not open for 3rd-party implementation.

{

 "name": "3rd-party Keyboard",
 "description": "3rd-party Keyboard",
 "type": "privilege",
 "role": "input",
 "launch_path": "/settings.html",
 "developer": {
   "name": "developer's name",
   "url": "https://keyboard.example.com"
 },
 "permissions": {
   "input": {}
 },
 "inputs": {
   "en": {
     "launch_path": "/index.html#en",
     "name": "English",
     "description": "English layout",
     "types": ["url", "text"],
     "locales": {
       "en-US": {
         "name": "English",
         "description": "English layout"
       },
       "zh-TW": {
         "name": "英文",
         "description": "英文鍵盤"
       }
     }
   },
   "en-Dvorak": {
     "launch_path": "/index.html#en-Dvorak",
     "name": "English (Dvorak)",
     "description": "Dvorak layout",
     "types": ["url", "text"]
   },
   "es": {
     "launch_path": "/index.html#es",
     "name": "Spanish",
     "description": "Spanish layout",
     "types": ["url", "text"]
   },
   "pt-BR": {
     "launch_path": "/index.html#pt-BR",
     "name": "Portuguese Brazilian",
     "description": "Portuguese Brazilian layout",
     "types": ["url", "text"]
   },
   "pl": {
     "launch_path": "/index.html#pl",
     "name": "Polish",
     "description": "Polish layout",
     "types": ["url", "text"]
   },
   "ca": {
     "launch_path": "/index.html#ca",
     "name": "Catalan",
     "description": "Catalan layout",
     "types": ["url", "text"]
   },
   "cz": {
     "launch_path": "/index.html#cz",
     "name": "Czech",
     "description": "Czech layout",
     "types": ["url", "text"]
   },
   "fr": {
     "launch_path": "/index.html#fr",
     "name": "French",
     "description": "French layout",
     "types": ["url", "text"]
   },
   "de": {
     "launch_path": "/index.html#de",
     "name": "German",
     "description": "German layout",
     "types": ["url", "text"]
   },
   "nb": {
     "launch_path": "/index.html#nb",
     "name": "Norwegian Bokmal",
     "description": "Norwegian Bokmal layout",
     "types": ["url", "text"]
   },
   "sk": {
     "launch_path": "/index.html#sk",
     "name": "Slovak",
     "description": "Slovak layout",
     "types": ["url", "text"]
   },
   "tr-Q": {
     "launch_path": "/index.html#tr-Q",
     "name": "Turkish Q",
     "description": "Turkish Q layout",
     "types": ["url", "text"]
   },
   "tr-F": {
     "launch_path": "/index.html#tr-F",
     "name": "Turkish F",
     "description": "Turkish F layout",
     "types": ["url", "text"]
   },
   "ro": {
     "launch_path": "/index.html#ro",
     "name": "Romanian",
     "description": "Romanian layout",
     "types": ["url", "text"]
   },
   "ru": {
     "launch_path": "/index.html#ru",
     "name": "Russian",
     "description": "Russian layout",
     "types": ["url", "text"]
   },
   "sr-Cyrl": {
     "launch_path": "/index.html#sr-Cyrl",
     "name": "Serbian (Cyrillic)",
     "description": "Serbian (Cyrillic) layout",
     "types": ["url", "text"]
   },
   "sr-Latn": {
     "launch_path": "/index.html#sr-Latn",
     "name": "Serbian (Latin)",
     "description": "Serbian (Latin) layout",
     "types": ["url", "text"]
   },
   "ar": {
     "launch_path": "/index.html#ar",
     "name": "Arabic",
     "description": "Arabic layout",
     "types": ["url", "text"]
   },
   "he": {
     "launch_path": "/index.html#he",
     "name": "Hebrew",
     "description": "Hebrew layout",
     "types": ["url", "text"]
   },
   "hu": {
     "launch_path": "/index.html#hu",
     "name": "Hungarian",
     "description": "Hungarian layout",
     "types": ["url", "text"]
   },
   "el": {
     "launch_path": "/index.html#el",
     "name": "Greek",
     "description": "Greek layout",
     "types": ["url", "text"]
   },
   "zh-Hans-Pinyin": {
     "launch_path": "/index.html#zh-Hans-Pinyin",
     "name": "Pinyin",
     "description": "Pinyin",
     "types": ["url", "text"]
   },
   "number": {
     "launch_path": "/index.html#numberLayout",
     "name": "Number",
     "description": "Number layout",
     "types": ["number"]
   }
 },
 "locales": {
   "en-US": {
     "name": "3rd-party Keyboard",
     "description": "3rd-party Keyboard"
   },
   "zh-TW": {
     "name": "第三方鍵盤",
     "description": "第三方鍵盤"
   }
 },
 "default_locale": "en-US"

}

Layout Matching Algorithm

When an input field is focused, if its type attribute is one of the allowed values stated above, it will be used to filter a set of candidate layouts. A candidate layout means it can handle this input type or is possible to let user input all characters that this input field can accept. For example, if the type of a input is "url", then a layout with "url" or "text" listed in the types of its manifest will be matched. However, if a input field with type "text", then all layouts that support "text" will be matched, but those layouts that only support "url" will not. This is because we believe layouts that can handle "text" could be a fallback for "url" input type, but not vice versa. We also believe "text" could be a fallback for all allowed types stated above.

The matching algorithm of keyboard manager in System app is as follows:

  1. With the given type, find all layouts claims to support the said type and put it into the list.
  2. Next, find layouts claims to support "text" and put it into the list. Layouts do not get duplicated listing even if it supports both types.
  3. Present the user with the choice of the layouts available to handle the input field. The order of presenting list is depend on UX design and/or user preferences in Settings.

Proposed API

The input method API is available to web content who intend to implement an input method, or "input source", or "virtual keyboard".

partial interface Navigator {
    readonly attribute InputMethod inputMethod;
};
interface InputMethod: EventTarget {
    // Input Method Manager contain a few global methods expose to apps
    readonly attribute InputMethodManager mgmt;

    // Fired when the input context changes, include changes from and to null.
    // The new InputContext instance will be available in the event object under |inputcontext| property.
    // When it changes to null it means the app (the user of this API) no longer has the control of the original focused input field.
    // Note that if the app saves the original context, it might get void; implementation decides when to void the input context.
    attribute EventHandler oninputcontextchange;

    // An "input context" is mapped to a text field that the app is allow to mutate.
    // this attribute should be null when there is no text field currently focused.
    readonly attribute InputContext? inputcontext;
};
// Manages the list of IMEs, enables/disables IME and switches to an IME.
interface InputMethodManager {
    // Ask the OS to show a list of available IMEs for users to switch from.
    // OS should ignore this request if the app is currently not the active one.
    void showAll();

    // Ask the OS to switch away from the current active Keyboard app.
    // OS should ignore this request if the app is currently not the active one.
    void next();

    // To know if the OS supports IME switching or not.
    // Use case: let the keyboard app knows if it is necessary to show the "IME switching"
    // (globe) button. We have a use case that when there is only one IME enabled, we
    // should not show the globe icon.
    boolean supportsSwitching();

    // Ask the OS to hide the current active Keyboard app. (was: |removeFocus()|)
    // OS should ignore this request if the app is currently not the active one.
    // The OS will void the current input context (if it exists).
    // This method belong to |mgmt| because we would like to allow Keyboard to access to
    // this method w/o a input context.
    void hide();
 };
// The input context, which consists of attributes and information of current input field.
// It also hosts the methods available to the keyboard app to mutate the input field represented.
// An "input context" gets void when the app is no longer allowed to interact with the text field,
// e.g., the text field does no longer exist, the app is being switched to background, and etc.
// [JJ] I doubt whether we should have 'name', 'type', etc. here. In the manifest we should
//      have entry points where the keyboard specifies which view to load when going into a
//      certain context. Requiring to do this manually will give extra work.
//      The system should guarantee that the right view is rendered based on entry_points in
//      in manifest (e.g. navigate keyboard to #text/en, or something, based on manifest.
// [Tim] I don't think they are exclusive. A keyboard app might choose to load the same page with the same hash
//      for different types but only to deal with the |type| or |inputmode| difference later.
// [JS] I agree that exposing type etc is a good idea. It's quite likely that the same keyboard
//      app will want to handle multiple different keyboards, for example both for latin text as well as
//      numeric keyboard.
//      But I agree that also enabling the keyboard to declare in the manifest which types it supports
//      is a good idea.
interface InputContext: EventTarget {
   // The tag name of input field, which is enum of "input", "textarea", or "contenteditable"
   readonly DOMString type;

   // The type of the input field, which is enum of text, number, password, url, search, email, and so on.
   // See http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#states-of-the-type-attribute
   readonly DOMString inputType;

   /*
    * The inputmode string, representing the input mode.
    * See http://www.whatwg.org/specs/web-apps/current-work/multipage/association-of-controls-and-forms.html#input-modalities:-the-inputmode-attribute
    */
   readonly DOMString inputMode;

   /*
    * The primary language for the input field.
    * It is the value of HTMLElement.lang.
    * See http://www.whatwg.org/specs/web-apps/current-work/multipage/elements.html#htmlelement
    */
   readonly DOMString lang;

   /*
    * Get specified range of text content from the input field.
     * @param start The start position of the range. Defaults to 0.
     * @param length The range length. Defaults to the end of the text.
    */
   Promise<DOMString> getText(optional start, optional length);

   // The start and stop position of the selection.
   readonly attribute long selectionStart;
   readonly attribute long selectionEnd;

   // The text before and after the begining of the selected text.
   readonly attribute DOMString textBeforeCursor;
   readonly attribute DOMString textAfterCursor;

    /*
     * Set the selection range of the the editable text.
     * Note: This method cannot be used to move the cursor during composition. Calling this
     * method will cancel composition.
     * @param start The beginning of the selected text.
     * @param length The length of the selected text.
     *
     * Note that the start position should be less or equal to the end position.
     * To move the cursor, set the start and end position to the same value.
     *
     * [JJ] I think that this method should return the same info as the selectionchange event
     *      rather than a boolean.
     * [yxl] I don't think so. We could get selection range info by checking the attributes of 
     *      selectionStart and selectionEnd.
     */
    Promise<void> setSelectionRange(long start, long length);

    /* User moves the cursor, or changes the selection with other means. If the text around
     * cursor has changed, but the cursor has not been moved, the IME won't get notification.
     */
    attribute EventHandler onselectionchange;

    /*
     * Commit text to current input field and replace text around cursor position. It will clear the current composition.
     *
     * @param text The string to be replaced with.
     * @param offset The offset from the cursor position where replacing starts. Defaults to 0.
     * @param length The length of text to replace. Defaults to 0.
     */
     Promise<void> replaceSurroundingText(DOMString text, optional long offset, optional long length);

    /*
     *
     * Delete text around the cursor.
     * @param offset The offset from the cursor position where deletion starts.
     * @param length The length of text to delete.
     */
    Promise<void> deleteSurroundingText(long offset, long length);

    /*
    * Notifies when the text around the cursor is changed, due to either text
    * editing or cursor movement. If the cursor has been moved, but the text around has not
    * changed, the IME won't get notification.
    */
    // [JS] Can you describe how the cursor can be moved without the surrounding text
    //      also changing? Is that really something that can happen?
    // [yxl] For example, if the text field is filled with 'a', wherever the cusor movies the surrounding text is always 'aa...'. Another exmaple, the selection range is changed, but the cursor isn't and the surrouding text won't be changed.
    attribute EventHandler onsurroundingtextchange;

    /*
      * send a character with its key events.
      * @param modifiers see http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/base/nsIDOMWindowUtils.idl#206
      * @return true if succeeds. Otherwise false if the input context becomes invalid.
      * Alternative: sendKey(KeyboardEvent event), but we will likely waste memory for creating the KeyboardEvent object.
      */
    Promise<void> sendKey(long keyCode, long charCode, optional long modifiers);

    /*
     * Set current composing text. This method will start composition or update composition if it
     * has started. The composition will be started right before the cursor position and any
     * selected text will be replaced by the composing text. When the composition is started, 
     * calling this method can update the text and move cursor winthin the range of the composing
     * text.
     * @param text The new composition text to show.
     * @param cursor The new cursor position relative to the start of the composition text. The cursor should
     * be positioned within the composition text. This means the value should be >= 0 and <= the length of
     * composition text. Defaults to the lenght of composition text, i.e., the cursor will be positioned after
     * the composition text.
     *
     * The composing text, which is shown with underlined style to distinguish from the existing text, is used
     * to compose non-ASCII characters from keystrokes, e.g. Pinyin or Hiragana. The composing text is the
     * intermediate text to help input complex character and is not committed to current input field. Therefore
     * if any text operation other than composition is performed, the composition will automatically
     * end. Same apply when the inputContext is lost during a unfinished composition session.
     *
     * To finish composition and commit text to current input field, an IME should call |endComposition|.
     */
    Promise<void> setComposition(DOMString text, optional long cursor);

    /*
     * End composition, clear the composing text and commit given text to current input field. The text will
     * be committed before the cursor position.
     * @param text The text to commited before cursor position. If empty string is given, no text will be
     * committed.
     *
     * Note that composition always ends automatically if the composition does not
     * explicitly end by calling |endComposition|, but is interrupted by |sendKey|, |setSelectionRange|,
     * |replaceSurroundingText|, |deleteSurroundingText|, user moving the cursor, changing the focus, etc.
     */
    Promise<void> endComposition(optional DOMString text);
};

Use cases for each of the methods

  • For a simple virtual keyboard action (send a character and key events w/ each user action), use sendKey(). TODO: should we allow backspace key to be sent from the method? If not, how do send these non-printable characters and it's effect with key events?
  • [yxl] I perfer to allowing non-printable character, such as backspace key, to be sent, if there is no security issue. This
  • would give the IME more flexibility.
  • For spellcheck, autocomplete etc, use surrounding text methods.
  • For cursor moment helper features, use setSelectionRange() and related attributes.
  • For Asian IMEs that sends characters and composition along with the composition events, use setComposition() and endComposition().

It is important to stick with the given use cases because the web application might need to react with what the user actually do. To test the events currently sent to the web, see http://jsfiddle.net/timdream/YDGgk/ .

Examples

The following "snowman filler" Keyboard app will start filling snowman character ("☃") and follow by characters "SNOW" with key events to the input field whenever the user is focus on a input field and switch to the keyboard app.

If the field is a numeric field, it will fill "1337".

var timer;
function startTyping(inputContext) {
  clearTimeout(timer);
  timer = setInterval(function typing() {
    /* [JJ] So I think that this code shouldn't be here, because you'll get lots of clutter
     *      as you'll also have to take languages into account.
     *      Rather rely on entry points in manifest...
     */

    if (inputContext.inputmode === 'numeric' || inputContext.type === 'number') {
      ['1', '3', '3', '7'].forEach(function (k) {
        // For numbers, keyCode is same as the charCode.
        inputContext.sendKey(k.charCodeAt(0), k.charCodeAt(0));
      });
    } else {
      // It's not a good idea to commit text w/o sending events. So we should first send composition events.
      inputContext.setComposition('☃');
      // end the composition and commit the text.
      inputContext.endComposition('☃');
      ['S', 'N', 'O', 'W'].forEach(function (k) {
        // For capital Latin letters, keyCode is same as the charCode.
        inputContext.sendKey(k.charCodeAt(0), k.charCodeAt(0));
      });
  }, 1000);
}

function stopTyping() {
  clearTimeout(timer);
}

var im = navigator.inputMethod;

im.addEventListener('inputcontextchange', function contextchanged(evt) {
  if (evt.inputcontext) {
     // Got a new context, start working with it.
     startTyping(evt.inputcontext);
  } else {
     // The user have removed the focus, we are not allow to type into the text field anymore.
     stopTyping();
  }
});

if (im.inputcontext) {
  // The webpage here is loaded *after* the user has place the focus on the text field,
  // let's start typing now.
  startTyping(im.inputcontext);
}

Related

Android IME API:

http://developer.android.com/guide/topics/text/creating-input-method.html#IMEAPI

iOS Keyboard Management:

http://developer.apple.com/library/ios/#documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/KeyboardManagement/KeyboardManagement.html#//apple_ref/doc/uid/TP40009542-CH5-SW1

Chrome Extensions API:

http://developer.chrome.com/trunk/extensions/input.ime.html