mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-09 11:25:00 +00:00
Merge m-c to inbound, a=merge
This commit is contained in:
commit
8dafd1ef34
@ -483,7 +483,15 @@ toolbar:not(#TabsToolbar) > #personal-bookmarks {
|
||||
list-style-image: none;
|
||||
}
|
||||
|
||||
#urlbar:not([actiontype="switchtab"]) > #urlbar-display-box {
|
||||
#urlbar:not([actiontype="switchtab"]):not([actiontype="extension"]) > #urlbar-display-box {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#urlbar:not([actiontype="switchtab"]) > #urlbar-display-box > #switchtab {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#urlbar:not([actiontype="extension"]) > #urlbar-display-box > #extension {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
@ -760,7 +760,8 @@
|
||||
</hbox>
|
||||
</box>
|
||||
<box id="urlbar-display-box" align="center">
|
||||
<label class="urlbar-display urlbar-display-switchtab" value="&urlbar.switchToTab.label;"/>
|
||||
<label id="switchtab" class="urlbar-display urlbar-display-switchtab" value="&urlbar.switchToTab.label;"/>
|
||||
<label id="extension" class="urlbar-display urlbar-display-extension" value="&urlbar.extension.label;"/>
|
||||
</box>
|
||||
<hbox id="urlbar-icons">
|
||||
<image id="page-report-button"
|
||||
|
@ -56,6 +56,11 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
<field name="AppConstants" readonly="true">
|
||||
(Components.utils.import("resource://gre/modules/AppConstants.jsm", {})).AppConstants;
|
||||
</field>
|
||||
|
||||
<field name="ExtensionSearchHandler" readonly="true">
|
||||
(Components.utils.import("resource://gre/modules/ExtensionSearchHandler.jsm", {})).ExtensionSearchHandler;
|
||||
</field>
|
||||
|
||||
<constructor><![CDATA[
|
||||
this._prefs = Components.classes["@mozilla.org/preferences-service;1"]
|
||||
.getService(Components.interfaces.nsIPrefService)
|
||||
@ -174,6 +179,10 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
returnValue = action.params.input;
|
||||
break;
|
||||
}
|
||||
case "extension": {
|
||||
returnValue = action.params.content;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let originalUrl = ReaderMode.getOriginalUrl(aValue);
|
||||
@ -478,6 +487,13 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
actionDetails
|
||||
);
|
||||
break;
|
||||
case "extension":
|
||||
this.handleRevert();
|
||||
// Give the extension control of handling the command.
|
||||
let searchString = action.params.content;
|
||||
let keyword = action.params.keyword;
|
||||
this.ExtensionSearchHandler.handleInputEntered(keyword, searchString, where);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// This is a fallback for add-ons and old testing code that directly
|
||||
@ -595,7 +611,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
engineOrEngineName;
|
||||
let isOneOff = this.popup.oneOffSearchButtons
|
||||
.maybeRecordTelemetry(event, openUILinkWhere, openUILinkParams);
|
||||
// Infer the type of the even which triggered the search.
|
||||
// Infer the type of the event which triggered the search.
|
||||
let eventType = "unknown";
|
||||
if (event instanceof KeyboardEvent) {
|
||||
eventType = "key";
|
||||
@ -1173,6 +1189,9 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
this._clearNoActions();
|
||||
this.formatValue();
|
||||
}
|
||||
if (ExtensionSearchHandler.hasActiveInputSession()) {
|
||||
ExtensionSearchHandler.handleInputCancelled();
|
||||
}
|
||||
]]></handler>
|
||||
|
||||
<handler event="dragstart" phase="capturing"><![CDATA[
|
||||
|
32
browser/components/extensions/ext-c-omnibox.js
Normal file
32
browser/components/extensions/ext-c-omnibox.js
Normal file
@ -0,0 +1,32 @@
|
||||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||
|
||||
var {
|
||||
runSafeSyncWithoutClone,
|
||||
SingletonEventManager,
|
||||
} = ExtensionUtils;
|
||||
|
||||
extensions.registerSchemaAPI("omnibox", "addon_child", context => {
|
||||
return {
|
||||
omnibox: {
|
||||
onInputChanged: new SingletonEventManager(context, "omnibox.onInputChanged", fire => {
|
||||
let listener = (text, id) => {
|
||||
runSafeSyncWithoutClone(fire, text, suggestions => {
|
||||
// TODO: Switch to using callParentFunctionNoReturn once bug 1314903 is fixed.
|
||||
context.childManager.callParentAsyncFunction("omnibox_internal.addSuggestions", [
|
||||
id,
|
||||
suggestions,
|
||||
]);
|
||||
});
|
||||
};
|
||||
context.childManager.getParentEvent("omnibox_internal.onInputChanged").addListener(listener);
|
||||
return () => {
|
||||
context.childManager.getParentEvent("omnibox_internal.onInputChanged").removeListener(listener);
|
||||
};
|
||||
}).api(),
|
||||
},
|
||||
};
|
||||
});
|
104
browser/components/extensions/ext-omnibox.js
Normal file
104
browser/components/extensions/ext-omnibox.js
Normal file
@ -0,0 +1,104 @@
|
||||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionSearchHandler",
|
||||
"resource://gre/modules/ExtensionSearchHandler.jsm");
|
||||
var {
|
||||
SingletonEventManager,
|
||||
} = ExtensionUtils;
|
||||
|
||||
// WeakMap[extension -> keyword]
|
||||
let gKeywordMap = new WeakMap();
|
||||
|
||||
/* eslint-disable mozilla/balanced-listeners */
|
||||
extensions.on("manifest_omnibox", (type, directive, extension, manifest) => {
|
||||
let keyword = manifest.omnibox.keyword;
|
||||
try {
|
||||
// This will throw if the keyword is already registered.
|
||||
ExtensionSearchHandler.registerKeyword(keyword, extension);
|
||||
gKeywordMap.set(extension, keyword);
|
||||
} catch (e) {
|
||||
extension.manifestError(e.message);
|
||||
}
|
||||
});
|
||||
|
||||
extensions.on("shutdown", (type, extension) => {
|
||||
let keyword = gKeywordMap.get(extension);
|
||||
if (keyword) {
|
||||
ExtensionSearchHandler.unregisterKeyword(keyword);
|
||||
gKeywordMap.delete(extension);
|
||||
}
|
||||
});
|
||||
/* eslint-enable mozilla/balanced-listeners */
|
||||
|
||||
extensions.registerSchemaAPI("omnibox", "addon_parent", context => {
|
||||
let {extension} = context;
|
||||
return {
|
||||
omnibox: {
|
||||
setDefaultSuggestion(suggestion) {
|
||||
let keyword = gKeywordMap.get(extension);
|
||||
try {
|
||||
// This will throw if the keyword failed to register.
|
||||
ExtensionSearchHandler.setDefaultSuggestion(keyword, suggestion);
|
||||
} catch (e) {
|
||||
return Promise.reject(e.message);
|
||||
}
|
||||
},
|
||||
|
||||
onInputStarted: new SingletonEventManager(context, "omnibox.onInputStarted", fire => {
|
||||
let listener = (eventName) => {
|
||||
fire();
|
||||
};
|
||||
extension.on(ExtensionSearchHandler.MSG_INPUT_STARTED, listener);
|
||||
return () => {
|
||||
extension.off(ExtensionSearchHandler.MSG_INPUT_STARTED, listener);
|
||||
};
|
||||
}).api(),
|
||||
|
||||
onInputCancelled: new SingletonEventManager(context, "omnibox.onInputCancelled", fire => {
|
||||
let listener = (eventName) => {
|
||||
fire();
|
||||
};
|
||||
extension.on(ExtensionSearchHandler.MSG_INPUT_CANCELLED, listener);
|
||||
return () => {
|
||||
extension.off(ExtensionSearchHandler.MSG_INPUT_CANCELLED, listener);
|
||||
};
|
||||
}).api(),
|
||||
|
||||
onInputEntered: new SingletonEventManager(context, "omnibox.onInputEntered", fire => {
|
||||
let listener = (eventName, text, disposition) => {
|
||||
fire(text, disposition);
|
||||
};
|
||||
extension.on(ExtensionSearchHandler.MSG_INPUT_ENTERED, listener);
|
||||
return () => {
|
||||
extension.off(ExtensionSearchHandler.MSG_INPUT_ENTERED, listener);
|
||||
};
|
||||
}).api(),
|
||||
},
|
||||
|
||||
omnibox_internal: {
|
||||
addSuggestions(id, suggestions) {
|
||||
let keyword = gKeywordMap.get(extension);
|
||||
try {
|
||||
ExtensionSearchHandler.addSuggestions(keyword, id, suggestions);
|
||||
} catch (e) {
|
||||
// Silently fail because the extension developer can not know for sure if the user
|
||||
// has already invalidated the callback when asynchronously providing suggestions.
|
||||
}
|
||||
},
|
||||
|
||||
onInputChanged: new SingletonEventManager(context, "omnibox_internal.onInputChanged", fire => {
|
||||
let listener = (eventName, text, id) => {
|
||||
fire(text, id);
|
||||
};
|
||||
extension.on(ExtensionSearchHandler.MSG_INPUT_CHANGED, listener);
|
||||
return () => {
|
||||
extension.off(ExtensionSearchHandler.MSG_INPUT_CHANGED, listener);
|
||||
};
|
||||
}).api(),
|
||||
},
|
||||
};
|
||||
});
|
@ -5,6 +5,7 @@ category webextension-scripts commands chrome://browser/content/ext-commands.js
|
||||
category webextension-scripts contextMenus chrome://browser/content/ext-contextMenus.js
|
||||
category webextension-scripts desktop-runtime chrome://browser/content/ext-desktop-runtime.js
|
||||
category webextension-scripts history chrome://browser/content/ext-history.js
|
||||
category webextension-scripts omnibox chrome://browser/content/ext-omnibox.js
|
||||
category webextension-scripts pageAction chrome://browser/content/ext-pageAction.js
|
||||
category webextension-scripts sessions chrome://browser/content/ext-sessions.js
|
||||
category webextension-scripts tabs chrome://browser/content/ext-tabs.js
|
||||
@ -13,6 +14,7 @@ category webextension-scripts windows chrome://browser/content/ext-windows.js
|
||||
|
||||
# scripts that must run in the same process as addon code.
|
||||
category webextension-scripts-addon contextMenus chrome://browser/content/ext-c-contextMenus.js
|
||||
category webextension-scripts-addon omnibox chrome://browser/content/ext-c-omnibox.js
|
||||
category webextension-scripts-addon tabs chrome://browser/content/ext-c-tabs.js
|
||||
|
||||
# schemas
|
||||
@ -22,6 +24,7 @@ category webextension-schemas commands chrome://browser/content/schemas/commands
|
||||
category webextension-schemas context_menus chrome://browser/content/schemas/context_menus.json
|
||||
category webextension-schemas context_menus_internal chrome://browser/content/schemas/context_menus_internal.json
|
||||
category webextension-schemas history chrome://browser/content/schemas/history.json
|
||||
category webextension-schemas omnibox chrome://browser/content/schemas/omnibox.json
|
||||
category webextension-schemas page_action chrome://browser/content/schemas/page_action.json
|
||||
category webextension-schemas sessions chrome://browser/content/schemas/sessions.json
|
||||
category webextension-schemas tabs chrome://browser/content/schemas/tabs.json
|
||||
|
@ -18,10 +18,12 @@ browser.jar:
|
||||
content/browser/ext-contextMenus.js
|
||||
content/browser/ext-desktop-runtime.js
|
||||
content/browser/ext-history.js
|
||||
content/browser/ext-omnibox.js
|
||||
content/browser/ext-pageAction.js
|
||||
content/browser/ext-sessions.js
|
||||
content/browser/ext-tabs.js
|
||||
content/browser/ext-utils.js
|
||||
content/browser/ext-windows.js
|
||||
content/browser/ext-c-contextMenus.js
|
||||
content/browser/ext-c-omnibox.js
|
||||
content/browser/ext-c-tabs.js
|
||||
|
@ -9,6 +9,7 @@ browser.jar:
|
||||
content/browser/schemas/context_menus.json
|
||||
content/browser/schemas/context_menus_internal.json
|
||||
content/browser/schemas/history.json
|
||||
content/browser/schemas/omnibox.json
|
||||
content/browser/schemas/page_action.json
|
||||
content/browser/schemas/sessions.json
|
||||
content/browser/schemas/tabs.json
|
||||
|
248
browser/components/extensions/schemas/omnibox.json
Normal file
248
browser/components/extensions/schemas/omnibox.json
Normal file
@ -0,0 +1,248 @@
|
||||
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
[
|
||||
{
|
||||
"namespace": "manifest",
|
||||
"types": [
|
||||
{
|
||||
"$extend": "WebExtensionManifest",
|
||||
"properties": {
|
||||
"omnibox": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "$ref": "UnrecognizedProperty" },
|
||||
"properties": {
|
||||
"keyword": {
|
||||
"type": "string",
|
||||
"pattern": "^[^?\\s:]([^\\s:]*[^/\\s:])?$"
|
||||
}
|
||||
},
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"namespace": "omnibox",
|
||||
"description": "The omnibox API allows you to register a keyword with Firefox's address bar.",
|
||||
"permissions": ["manifest:omnibox"],
|
||||
"types": [
|
||||
{
|
||||
"id": "DescriptionStyleType",
|
||||
"type": "string",
|
||||
"description": "The style type.",
|
||||
"enum": ["url", "match", "dim"]
|
||||
},
|
||||
{
|
||||
"id": "OnInputEnteredDisposition",
|
||||
"type": "string",
|
||||
"enum": ["currentTab", "newForegroundTab", "newBackgroundTab"],
|
||||
"description": "The window disposition for the omnibox query. This is the recommended context to display results. For example, if the omnibox command is to navigate to a certain URL, a disposition of 'newForegroundTab' means the navigation should take place in a new selected tab."
|
||||
},
|
||||
{
|
||||
"id": "SuggestResult",
|
||||
"type": "object",
|
||||
"description": "A suggest result.",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "The text that is put into the URL bar, and that is sent to the extension when the user chooses this entry."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "The text that is displayed in the URL dropdown. Can contain XML-style markup for styling. The supported tags are 'url' (for a literal URL), 'match' (for highlighting text that matched what the user's query), and 'dim' (for dim helper text). The styles can be nested, eg. <dim><match>dimmed match</match></dim>. You must escape the five predefined entities to display them as text: stackoverflow.com/a/1091953/89484 "
|
||||
},
|
||||
"descriptionStyles": {
|
||||
"optional": true,
|
||||
"unsupported": true,
|
||||
"type": "array",
|
||||
"description": "An array of style ranges for the description, as provided by the extension.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "The style ranges for the description, as provided by the extension.",
|
||||
"properties": {
|
||||
"offset": { "type": "integer" },
|
||||
"type": { "description": "The style type", "$ref": "DescriptionStyleType"},
|
||||
"length": { "type": "integer", "optional": true }
|
||||
}
|
||||
}
|
||||
},
|
||||
"descriptionStylesRaw": {
|
||||
"optional": true,
|
||||
"unsupported": true,
|
||||
"type": "array",
|
||||
"description": "An array of style ranges for the description, as provided by ToValue().",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "The style ranges for the description, as provided by ToValue().",
|
||||
"properties": {
|
||||
"offset": { "type": "integer" },
|
||||
"type": { "type": "integer" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DefaultSuggestResult",
|
||||
"type": "object",
|
||||
"description": "A suggest result.",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "The text that is displayed in the URL dropdown."
|
||||
},
|
||||
"descriptionStyles": {
|
||||
"optional": true,
|
||||
"unsupported": true,
|
||||
"type": "array",
|
||||
"description": "An array of style ranges for the description, as provided by the extension.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "The style ranges for the description, as provided by the extension.",
|
||||
"properties": {
|
||||
"offset": { "type": "integer" },
|
||||
"type": { "description": "The style type", "$ref": "DescriptionStyleType"},
|
||||
"length": { "type": "integer", "optional": true }
|
||||
}
|
||||
}
|
||||
},
|
||||
"descriptionStylesRaw": {
|
||||
"optional": true,
|
||||
"unsupported": true,
|
||||
"type": "array",
|
||||
"description": "An array of style ranges for the description, as provided by ToValue().",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "The style ranges for the description, as provided by ToValue().",
|
||||
"properties": {
|
||||
"offset": { "type": "integer" },
|
||||
"type": { "type": "integer" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"functions": [
|
||||
{
|
||||
"name": "setDefaultSuggestion",
|
||||
"type": "function",
|
||||
"description": "Sets the description and styling for the default suggestion. The default suggestion is the text that is displayed in the first suggestion row underneath the URL bar.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "suggestion",
|
||||
"$ref": "DefaultSuggestResult",
|
||||
"description": "A partial SuggestResult object, without the 'content' parameter."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"name": "onInputStarted",
|
||||
"type": "function",
|
||||
"description": "User has started a keyword input session by typing the extension's keyword. This is guaranteed to be sent exactly once per input session, and before any onInputChanged events.",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"name": "onInputChanged",
|
||||
"type": "function",
|
||||
"description": "User has changed what is typed into the omnibox.",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "text"
|
||||
},
|
||||
{
|
||||
"name": "suggest",
|
||||
"type": "function",
|
||||
"description": "A callback passed to the onInputChanged event used for sending suggestions back to the browser.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "suggestResults",
|
||||
"type": "array",
|
||||
"description": "Array of suggest results",
|
||||
"items": {
|
||||
"$ref": "SuggestResult"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "onInputEntered",
|
||||
"type": "function",
|
||||
"description": "User has accepted what is typed into the omnibox.",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "text"
|
||||
},
|
||||
{
|
||||
"name": "disposition",
|
||||
"$ref": "OnInputEnteredDisposition"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "onInputCancelled",
|
||||
"type": "function",
|
||||
"description": "User has ended the keyword input session without accepting the input.",
|
||||
"parameters": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"namespace": "omnibox_internal",
|
||||
"description": "The internal namespace used by the omnibox API.",
|
||||
"defaultContexts": ["addon_parent_only"],
|
||||
"functions": [
|
||||
{
|
||||
"name": "addSuggestions",
|
||||
"type": "function",
|
||||
"async": "callback",
|
||||
"description": "Internal function used by omnibox.onInputChanged for adding search suggestions",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"description": "The ID of the callback received by onInputChangedInternal"
|
||||
},
|
||||
{
|
||||
"name": "suggestResults",
|
||||
"type": "array",
|
||||
"description": "Array of suggest results",
|
||||
"items": {
|
||||
"$ref": "omnibox.SuggestResult"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "callback",
|
||||
"optional": true,
|
||||
"parameters": []
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"name": "onInputChanged",
|
||||
"type": "function",
|
||||
"description": "Identical to omnibox.onInputChanged except no 'suggest' callback is provided.",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
@ -45,6 +45,7 @@ tags = webextensions
|
||||
[browser_ext_incognito_popup.js]
|
||||
[browser_ext_lastError.js]
|
||||
[browser_ext_legacy_extension_context_contentscript.js]
|
||||
[browser_ext_omnibox.js]
|
||||
[browser_ext_optionsPage_privileges.js]
|
||||
[browser_ext_pageAction_context.js]
|
||||
[browser_ext_pageAction_popup.js]
|
||||
|
@ -0,0 +1,286 @@
|
||||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
function* setup() {
|
||||
const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
|
||||
Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, false);
|
||||
|
||||
registerCleanupFunction(() => {
|
||||
Services.prefs.clearUserPref(SUGGEST_URLBAR_PREF);
|
||||
});
|
||||
}
|
||||
|
||||
add_task(function* () {
|
||||
let keyword = "test";
|
||||
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
"omnibox": {
|
||||
"keyword": keyword,
|
||||
},
|
||||
},
|
||||
|
||||
background: function() {
|
||||
browser.omnibox.onInputStarted.addListener(() => {
|
||||
browser.test.sendMessage("on-input-started-fired");
|
||||
});
|
||||
|
||||
let synchronous = true;
|
||||
let suggestions = null;
|
||||
let suggestCallback = null;
|
||||
|
||||
browser.omnibox.onInputChanged.addListener((text, suggest) => {
|
||||
if (synchronous && suggestions) {
|
||||
suggest(suggestions);
|
||||
} else {
|
||||
suggestCallback = suggest;
|
||||
}
|
||||
browser.test.sendMessage("on-input-changed-fired", {text});
|
||||
});
|
||||
|
||||
browser.omnibox.onInputCancelled.addListener(() => {
|
||||
browser.test.sendMessage("on-input-cancelled-fired");
|
||||
});
|
||||
|
||||
browser.omnibox.onInputEntered.addListener((text, disposition) => {
|
||||
browser.test.sendMessage("on-input-entered-fired", {text, disposition});
|
||||
});
|
||||
|
||||
browser.test.onMessage.addListener((msg, data) => {
|
||||
switch (msg) {
|
||||
case "set-suggestions":
|
||||
suggestions = data.suggestions;
|
||||
browser.test.sendMessage("suggestions-set");
|
||||
break;
|
||||
case "set-default-suggestion":
|
||||
browser.omnibox.setDefaultSuggestion(data.suggestion);
|
||||
browser.test.sendMessage("default-suggestion-set");
|
||||
break;
|
||||
case "set-synchronous":
|
||||
synchronous = data.synchronous;
|
||||
break;
|
||||
case "test-multiple-suggest-calls":
|
||||
suggestions.forEach(suggestion => suggestCallback([suggestion]));
|
||||
browser.test.sendMessage("test-ready");
|
||||
break;
|
||||
case "test-suggestions-after-delay":
|
||||
Promise.resolve().then(() => {
|
||||
suggestCallback(suggestions);
|
||||
browser.test.sendMessage("test-ready");
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function* expectEvent(event, expected = {}) {
|
||||
let actual = yield extension.awaitMessage(event);
|
||||
if (expected.text) {
|
||||
is(actual.text, expected.text,
|
||||
`Expected "${event}" to have fired with text: "${expected.text}".`);
|
||||
}
|
||||
if (expected.disposition) {
|
||||
is(actual.disposition, expected.disposition,
|
||||
`Expected "${event}" to have fired with disposition: "${expected.disposition}".`);
|
||||
}
|
||||
}
|
||||
|
||||
function* startInputSession() {
|
||||
gURLBar.focus();
|
||||
gURLBar.value = keyword;
|
||||
EventUtils.synthesizeKey(" ", {});
|
||||
yield expectEvent("on-input-started-fired");
|
||||
EventUtils.synthesizeKey("t", {});
|
||||
yield expectEvent("on-input-changed-fired", {text: "t"});
|
||||
return "t";
|
||||
}
|
||||
|
||||
function* testInputEvents() {
|
||||
gURLBar.focus();
|
||||
|
||||
// Start an input session by typing in <keyword><space>.
|
||||
for (let letter of keyword) {
|
||||
EventUtils.synthesizeKey(letter, {});
|
||||
}
|
||||
EventUtils.synthesizeKey(" ", {});
|
||||
yield expectEvent("on-input-started-fired");
|
||||
|
||||
// We should expect input changed events now that the keyword is active.
|
||||
EventUtils.synthesizeKey("b", {});
|
||||
yield expectEvent("on-input-changed-fired", {text: "b"});
|
||||
|
||||
EventUtils.synthesizeKey("c", {});
|
||||
yield expectEvent("on-input-changed-fired", {text: "bc"});
|
||||
|
||||
EventUtils.synthesizeKey("VK_BACK_SPACE", {});
|
||||
yield expectEvent("on-input-changed-fired", {text: "b"});
|
||||
|
||||
// Even though the input is <keyword><space> We should not expect an
|
||||
// input started event to fire since the keyword is active.
|
||||
EventUtils.synthesizeKey("VK_BACK_SPACE", {});
|
||||
yield expectEvent("on-input-changed-fired", {text: ""});
|
||||
|
||||
// Make the keyword inactive by hitting backspace.
|
||||
EventUtils.synthesizeKey("VK_BACK_SPACE", {});
|
||||
yield expectEvent("on-input-cancelled-fired");
|
||||
|
||||
// Activate the keyword by typing a space.
|
||||
// Expect onInputStarted to fire.
|
||||
EventUtils.synthesizeKey(" ", {});
|
||||
yield expectEvent("on-input-started-fired");
|
||||
|
||||
// onInputChanged should fire even if a space is entered.
|
||||
EventUtils.synthesizeKey(" ", {});
|
||||
yield expectEvent("on-input-changed-fired", {text: " "});
|
||||
|
||||
// The active session should cancel if the input blurs.
|
||||
gURLBar.blur();
|
||||
yield expectEvent("on-input-cancelled-fired");
|
||||
}
|
||||
|
||||
function* testHeuristicResult(expectedText, setDefaultSuggestion) {
|
||||
if (setDefaultSuggestion) {
|
||||
extension.sendMessage("set-default-suggestion", {
|
||||
suggestion: {
|
||||
description: expectedText,
|
||||
},
|
||||
});
|
||||
yield extension.awaitMessage("default-suggestion-set");
|
||||
}
|
||||
|
||||
let text = yield startInputSession();
|
||||
|
||||
let item = gURLBar.popup.richlistbox.children[0];
|
||||
|
||||
is(item.getAttribute("title"), expectedText,
|
||||
`Expected heuristic result to have title: "${expectedText}".`);
|
||||
|
||||
is(item.getAttribute("displayurl"), `${keyword} ${text}`,
|
||||
`Expected heuristic result to have displayurl: "${keyword} ${text}".`);
|
||||
|
||||
EventUtils.synthesizeMouseAtCenter(item, {});
|
||||
|
||||
yield expectEvent("on-input-entered-fired", {
|
||||
text,
|
||||
disposition: "currentTab",
|
||||
});
|
||||
}
|
||||
|
||||
function* testDisposition(suggestionIndex, expectedDisposition, expectedText) {
|
||||
yield startInputSession();
|
||||
|
||||
// Select the suggestion.
|
||||
for (let i = 0; i < suggestionIndex; i++) {
|
||||
EventUtils.synthesizeKey("VK_DOWN", {});
|
||||
}
|
||||
|
||||
let item = gURLBar.popup.richlistbox.children[suggestionIndex];
|
||||
if (expectedDisposition == "currentTab") {
|
||||
EventUtils.synthesizeMouseAtCenter(item, {});
|
||||
} else if (expectedDisposition == "newForegroundTab") {
|
||||
EventUtils.synthesizeMouseAtCenter(item, {accelKey: true});
|
||||
} else if (expectedDisposition == "newBackgroundTab") {
|
||||
EventUtils.synthesizeMouseAtCenter(item, {shiftKey: true, accelKey: true});
|
||||
}
|
||||
|
||||
yield expectEvent("on-input-entered-fired", {
|
||||
text: expectedText,
|
||||
disposition: expectedDisposition,
|
||||
});
|
||||
}
|
||||
|
||||
function* testSuggestions(info) {
|
||||
extension.sendMessage("set-synchronous", {synchronous: false});
|
||||
|
||||
function expectSuggestion({content, description}, index) {
|
||||
let item = gURLBar.popup.richlistbox.children[index + 1]; // Skip the heuristic result.
|
||||
|
||||
ok(!!item, "Expected item to exist");
|
||||
is(item.getAttribute("title"), description,
|
||||
`Expected suggestion to have title: "${description}".`);
|
||||
|
||||
is(item.getAttribute("displayurl"), `${keyword} ${content}`,
|
||||
`Expected suggestion to have displayurl: "${keyword} ${content}".`);
|
||||
}
|
||||
|
||||
let text = yield startInputSession();
|
||||
|
||||
extension.sendMessage(info.test);
|
||||
yield extension.awaitMessage("test-ready");
|
||||
|
||||
info.suggestions.forEach(expectSuggestion);
|
||||
|
||||
EventUtils.synthesizeMouseAtCenter(gURLBar.popup.richlistbox.children[0], {});
|
||||
yield expectEvent("on-input-entered-fired", {
|
||||
text,
|
||||
disposition: "currentTab",
|
||||
});
|
||||
}
|
||||
|
||||
yield setup();
|
||||
yield extension.startup();
|
||||
|
||||
yield testInputEvents();
|
||||
|
||||
// Test the heuristic result with default suggestions.
|
||||
yield testHeuristicResult("Generated extension", false /* setDefaultSuggestion */);
|
||||
yield testHeuristicResult("hello world", true /* setDefaultSuggestion */);
|
||||
yield testHeuristicResult("foo bar", true /* setDefaultSuggestion */);
|
||||
|
||||
let suggestions = [
|
||||
{content: "a", description: "select a"},
|
||||
{content: "b", description: "select b"},
|
||||
{content: "c", description: "select c"},
|
||||
];
|
||||
|
||||
extension.sendMessage("set-suggestions", {suggestions});
|
||||
yield extension.awaitMessage("suggestions-set");
|
||||
|
||||
// Test each suggestion and search disposition.
|
||||
yield testDisposition(1, "currentTab", suggestions[0].content);
|
||||
yield testDisposition(2, "newForegroundTab", suggestions[1].content);
|
||||
yield testDisposition(3, "newBackgroundTab", suggestions[2].content);
|
||||
|
||||
extension.sendMessage("set-suggestions", {suggestions});
|
||||
yield extension.awaitMessage("suggestions-set");
|
||||
|
||||
// Test adding suggestions asynchronously.
|
||||
yield testSuggestions({
|
||||
test: "test-multiple-suggest-calls",
|
||||
skipHeuristic: true,
|
||||
suggestions,
|
||||
});
|
||||
yield testSuggestions({
|
||||
test: "test-suggestions-after-delay",
|
||||
skipHeuristic: true,
|
||||
suggestions,
|
||||
});
|
||||
|
||||
// Start monitoring the console.
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
let waitForConsole = new Promise(resolve => {
|
||||
SimpleTest.monitorConsole(resolve, [{
|
||||
message: new RegExp(`The keyword provided is already registered: "${keyword}"`),
|
||||
}]);
|
||||
});
|
||||
|
||||
// Try registering another extension with the same keyword
|
||||
let extension2 = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
"omnibox": {
|
||||
"keyword": keyword,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
yield extension2.startup();
|
||||
|
||||
// Stop monitoring the console and confirm the correct errors are logged.
|
||||
SimpleTest.endMonitorConsole();
|
||||
yield waitForConsole;
|
||||
|
||||
yield extension2.unload();
|
||||
yield extension.unload();
|
||||
});
|
@ -10,6 +10,17 @@ XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
|
||||
const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
|
||||
const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
|
||||
|
||||
function* promiseAutocompleteResultPopup(inputText) {
|
||||
gURLBar.focus();
|
||||
gURLBar.value = inputText;
|
||||
gURLBar.controller.startSearch(inputText);
|
||||
yield promisePopupShown(gURLBar.popup);
|
||||
yield BrowserTestUtils.waitForCondition(() => {
|
||||
return gURLBar.controller.searchStatus >=
|
||||
Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH;
|
||||
});
|
||||
}
|
||||
|
||||
function* addBookmark(bookmark) {
|
||||
if (bookmark.keyword) {
|
||||
yield PlacesUtils.keywords.insert({
|
||||
@ -142,17 +153,9 @@ add_task(function* test_webnavigation_urlbar_bookmark_transitions() {
|
||||
|
||||
yield extension.awaitMessage("ready");
|
||||
|
||||
gURLBar.focus();
|
||||
gURLBar.value = "Bookmark To Click";
|
||||
gURLBar.controller.startSearch("Bookmark To Click");
|
||||
|
||||
let item;
|
||||
|
||||
yield BrowserTestUtils.waitForCondition(() => {
|
||||
item = gURLBar.popup.richlistbox.getItemAtIndex(1);
|
||||
return item;
|
||||
});
|
||||
yield promiseAutocompleteResultPopup("Bookmark To Click");
|
||||
|
||||
let item = gURLBar.popup.richlistbox.getItemAtIndex(1);
|
||||
item.click();
|
||||
yield extension.awaitFinish("webNavigation.from_address_bar.auto_bookmark");
|
||||
|
||||
@ -195,13 +198,7 @@ add_task(function* test_webnavigation_urlbar_keyword_transition() {
|
||||
|
||||
yield extension.awaitMessage("ready");
|
||||
|
||||
gURLBar.focus();
|
||||
gURLBar.value = "testkw search";
|
||||
gURLBar.controller.startSearch("testkw search");
|
||||
|
||||
yield BrowserTestUtils.waitForCondition(() => {
|
||||
return gURLBar.popup.input.controller.matchCount;
|
||||
});
|
||||
yield promiseAutocompleteResultPopup("testkw search");
|
||||
|
||||
let item = gURLBar.popup.richlistbox.getItemAtIndex(0);
|
||||
item.click();
|
||||
@ -242,14 +239,7 @@ add_task(function* test_webnavigation_urlbar_search_transitions() {
|
||||
yield extension.awaitMessage("ready");
|
||||
|
||||
yield prepareSearchEngine();
|
||||
|
||||
gURLBar.focus();
|
||||
gURLBar.value = "foo";
|
||||
gURLBar.controller.startSearch("foo");
|
||||
|
||||
yield BrowserTestUtils.waitForCondition(() => {
|
||||
return gURLBar.popup.input.controller.matchCount;
|
||||
});
|
||||
yield promiseAutocompleteResultPopup("foo");
|
||||
|
||||
let item = gURLBar.popup.richlistbox.getItemAtIndex(0);
|
||||
item.click();
|
||||
|
@ -0,0 +1,61 @@
|
||||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
function* testKeyword(params) {
|
||||
let normalized = yield ExtensionTestUtils.normalizeManifest({
|
||||
"omnibox": {
|
||||
"keyword": params.keyword,
|
||||
},
|
||||
});
|
||||
|
||||
if (params.expectError) {
|
||||
let expectedError = (
|
||||
String.raw`omnibox.keyword: String "${params.keyword}" ` +
|
||||
String.raw`must match /^[^?\s:]([^\s:]*[^/\s:])?$/`
|
||||
);
|
||||
ok(normalized.error.includes(expectedError),
|
||||
`The manifest error ${JSON.stringify(normalized.error)} ` +
|
||||
`must contain ${JSON.stringify(expectedError)}`);
|
||||
} else {
|
||||
equal(normalized.error, undefined, "Should not have an error");
|
||||
equal(normalized.errors.length, 0, "Should not have warnings");
|
||||
}
|
||||
}
|
||||
|
||||
add_task(function* test_manifest_commands() {
|
||||
// accepted single character keywords
|
||||
yield testKeyword({keyword: "a", expectError: false});
|
||||
yield testKeyword({keyword: "-", expectError: false});
|
||||
yield testKeyword({keyword: "嗨", expectError: false});
|
||||
yield testKeyword({keyword: "*", expectError: false});
|
||||
yield testKeyword({keyword: "/", expectError: false});
|
||||
|
||||
// rejected single character keywords
|
||||
yield testKeyword({keyword: "?", expectError: true});
|
||||
yield testKeyword({keyword: " ", expectError: true});
|
||||
yield testKeyword({keyword: ":", expectError: true});
|
||||
|
||||
// accepted multi-character keywords
|
||||
yield testKeyword({keyword: "aa", expectError: false});
|
||||
yield testKeyword({keyword: "http", expectError: false});
|
||||
yield testKeyword({keyword: "f?a", expectError: false});
|
||||
yield testKeyword({keyword: "fa?", expectError: false});
|
||||
yield testKeyword({keyword: "f/x", expectError: false});
|
||||
yield testKeyword({keyword: "/fx", expectError: false});
|
||||
|
||||
// rejected multi-character keywords
|
||||
yield testKeyword({keyword: " a", expectError: true});
|
||||
yield testKeyword({keyword: "a ", expectError: true});
|
||||
yield testKeyword({keyword: " ", expectError: true});
|
||||
yield testKeyword({keyword: " a ", expectError: true});
|
||||
yield testKeyword({keyword: "?fx", expectError: true});
|
||||
yield testKeyword({keyword: "fx/", expectError: true});
|
||||
yield testKeyword({keyword: "f:x", expectError: true});
|
||||
yield testKeyword({keyword: "fx:", expectError: true});
|
||||
yield testKeyword({keyword: "f x", expectError: true});
|
||||
|
||||
// miscellaneous tests
|
||||
yield testKeyword({keyword: "こんにちは", expectError: false});
|
||||
yield testKeyword({keyword: "http://", expectError: true});
|
||||
});
|
@ -7,4 +7,5 @@ tags = webextensions
|
||||
[test_ext_bookmarks.js]
|
||||
[test_ext_history.js]
|
||||
[test_ext_manifest_commands.js]
|
||||
[test_ext_manifest_omnibox.js]
|
||||
[test_ext_manifest_permissions.js]
|
||||
|
@ -421,6 +421,7 @@ BrowserGlue.prototype = {
|
||||
tag: 7,
|
||||
visiturl: 8,
|
||||
remotetab: 9,
|
||||
extension: 10,
|
||||
};
|
||||
if (actionType in buckets) {
|
||||
Services.telemetry
|
||||
|
@ -392,6 +392,7 @@ These should match what Safari and other Apple applications use on OS X Lion. --
|
||||
<!ENTITY openCmd.commandkey "l">
|
||||
<!ENTITY urlbar.placeholder2 "Search or enter address">
|
||||
<!ENTITY urlbar.accesskey "d">
|
||||
<!ENTITY urlbar.extension.label "Extension:">
|
||||
<!ENTITY urlbar.switchToTab.label "Switch to tab:">
|
||||
|
||||
<!ENTITY urlbar.searchSuggestionsNotification.question "Would you like to improve your search experience with suggestions?">
|
||||
|
@ -79,6 +79,13 @@
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
#urlbar[actiontype="extension"] > #identity-box > #identity-icon {
|
||||
-moz-image-region: inherit;
|
||||
list-style-image: url(chrome://browser/skin/addons/addon-install-anchor.svg);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* SHARING ICON */
|
||||
|
||||
#sharing-icon {
|
||||
|
@ -11,9 +11,14 @@ const LAYOUT_ERRORS_L10N =
|
||||
// displayed below it.
|
||||
|
||||
const EXPECTED_PROPERTIES = [
|
||||
"background-attachment",
|
||||
"background-clip",
|
||||
"background-color",
|
||||
"background-image",
|
||||
"background-origin",
|
||||
"background-position-x",
|
||||
"background-position-y",
|
||||
"background-repeat",
|
||||
"background-size",
|
||||
"border-bottom-left-radius",
|
||||
"border-bottom-right-radius",
|
||||
|
@ -177,7 +177,47 @@ var gTests = [
|
||||
value(1, '4px', 'replace') ] },
|
||||
{ property: 'border-top-width',
|
||||
values: [ value(0, '3px', 'replace', 'linear'),
|
||||
value(1, '4px', 'replace') ] } ]
|
||||
value(1, '4px', 'replace') ] },
|
||||
{ property: 'border-bottom-style',
|
||||
values: [ value(0, 'dotted', 'replace', 'linear'),
|
||||
value(1, 'dashed', 'replace') ] },
|
||||
{ property: 'border-left-style',
|
||||
values: [ value(0, 'solid', 'replace', 'linear'),
|
||||
value(1, 'solid', 'replace') ] },
|
||||
{ property: 'border-right-style',
|
||||
values: [ value(0, 'dotted', 'replace', 'linear'),
|
||||
value(1, 'dashed', 'replace') ] },
|
||||
{ property: 'border-top-style',
|
||||
values: [ value(0, 'dotted', 'replace', 'linear'),
|
||||
value(1, 'dashed', 'replace') ] },
|
||||
{ property: 'border-image-outset',
|
||||
values: [ value(0, '0 0 0 0', 'replace', 'linear'),
|
||||
value(1, '0 0 0 0', 'replace') ] },
|
||||
{ property: 'border-image-repeat',
|
||||
values: [ value(0, 'stretch stretch', 'replace', 'linear'),
|
||||
value(1, 'stretch stretch', 'replace') ] },
|
||||
{ property: 'border-image-slice',
|
||||
values: [ value(0, '100% 100% 100% 100%',
|
||||
'replace', 'linear'),
|
||||
value(1, '100% 100% 100% 100%', 'replace') ] },
|
||||
{ property: 'border-image-source',
|
||||
values: [ value(0, 'none', 'replace', 'linear'),
|
||||
value(1, 'none', 'replace') ] },
|
||||
{ property: 'border-image-width',
|
||||
values: [ value(0, '1 1 1 1', 'replace', 'linear'),
|
||||
value(1, '1 1 1 1', 'replace') ] },
|
||||
{ property: '-moz-border-bottom-colors',
|
||||
values: [ value(0, 'none', 'replace', 'linear'),
|
||||
value(1, 'none', 'replace') ] },
|
||||
{ property: '-moz-border-left-colors',
|
||||
values: [ value(0, 'none', 'replace', 'linear'),
|
||||
value(1, 'none', 'replace') ] },
|
||||
{ property: '-moz-border-right-colors',
|
||||
values: [ value(0, 'none', 'replace', 'linear'),
|
||||
value(1, 'none', 'replace') ] },
|
||||
{ property: '-moz-border-top-colors',
|
||||
values: [ value(0, 'none', 'replace', 'linear'),
|
||||
value(1, 'none', 'replace') ] } ]
|
||||
},
|
||||
{ desc: 'a property-indexed keyframe where a greater shorthand precedes'
|
||||
+ ' a lesser shorthand',
|
||||
@ -208,7 +248,47 @@ var gTests = [
|
||||
value(1, '4px', 'replace') ] },
|
||||
{ property: 'border-top-width',
|
||||
values: [ value(0, '3px', 'replace', 'linear'),
|
||||
value(1, '4px', 'replace') ] } ]
|
||||
value(1, '4px', 'replace') ] },
|
||||
{ property: 'border-bottom-style',
|
||||
values: [ value(0, 'dotted', 'replace', 'linear'),
|
||||
value(1, 'dashed', 'replace') ] },
|
||||
{ property: 'border-left-style',
|
||||
values: [ value(0, 'solid', 'replace', 'linear'),
|
||||
value(1, 'solid', 'replace') ] },
|
||||
{ property: 'border-right-style',
|
||||
values: [ value(0, 'dotted', 'replace', 'linear'),
|
||||
value(1, 'dashed', 'replace') ] },
|
||||
{ property: 'border-top-style',
|
||||
values: [ value(0, 'dotted', 'replace', 'linear'),
|
||||
value(1, 'dashed', 'replace') ] },
|
||||
{ property: 'border-image-outset',
|
||||
values: [ value(0, '0 0 0 0', 'replace', 'linear'),
|
||||
value(1, '0 0 0 0', 'replace') ] },
|
||||
{ property: 'border-image-repeat',
|
||||
values: [ value(0, 'stretch stretch', 'replace', 'linear'),
|
||||
value(1, 'stretch stretch', 'replace') ] },
|
||||
{ property: 'border-image-slice',
|
||||
values: [ value(0, '100% 100% 100% 100%',
|
||||
'replace', 'linear'),
|
||||
value(1, '100% 100% 100% 100%', 'replace') ] },
|
||||
{ property: 'border-image-source',
|
||||
values: [ value(0, 'none', 'replace', 'linear'),
|
||||
value(1, 'none', 'replace') ] },
|
||||
{ property: 'border-image-width',
|
||||
values: [ value(0, '1 1 1 1', 'replace', 'linear'),
|
||||
value(1, '1 1 1 1', 'replace') ] },
|
||||
{ property: '-moz-border-bottom-colors',
|
||||
values: [ value(0, 'none', 'replace', 'linear'),
|
||||
value(1, 'none', 'replace') ] },
|
||||
{ property: '-moz-border-left-colors',
|
||||
values: [ value(0, 'none', 'replace', 'linear'),
|
||||
value(1, 'none', 'replace') ] },
|
||||
{ property: '-moz-border-right-colors',
|
||||
values: [ value(0, 'none', 'replace', 'linear'),
|
||||
value(1, 'none', 'replace') ] },
|
||||
{ property: '-moz-border-top-colors',
|
||||
values: [ value(0, 'none', 'replace', 'linear'),
|
||||
value(1, 'none', 'replace') ] } ]
|
||||
},
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
@ -448,7 +528,47 @@ var gTests = [
|
||||
value(1, '3px', 'replace') ] },
|
||||
{ property: 'border-top-width',
|
||||
values: [ value(0, '2px', 'replace', 'linear'),
|
||||
value(1, '3px', 'replace') ] } ]
|
||||
value(1, '3px', 'replace') ] },
|
||||
{ property: 'border-bottom-style',
|
||||
values: [ value(0, 'dotted', 'replace', 'linear'),
|
||||
value(1, 'dashed', 'replace') ] },
|
||||
{ property: 'border-left-style',
|
||||
values: [ value(0, 'solid', 'replace', 'linear'),
|
||||
value(1, 'dashed', 'replace') ] },
|
||||
{ property: 'border-right-style',
|
||||
values: [ value(0, 'dotted', 'replace', 'linear'),
|
||||
value(1, 'dashed', 'replace') ] },
|
||||
{ property: 'border-top-style',
|
||||
values: [ value(0, 'dotted', 'replace', 'linear'),
|
||||
value(1, 'dashed', 'replace') ] },
|
||||
{ property: 'border-image-outset',
|
||||
values: [ value(0, '0 0 0 0', 'replace', 'linear'),
|
||||
value(1, '0 0 0 0', 'replace') ] },
|
||||
{ property: 'border-image-repeat',
|
||||
values: [ value(0, 'stretch stretch', 'replace', 'linear'),
|
||||
value(1, 'stretch stretch', 'replace') ] },
|
||||
{ property: 'border-image-slice',
|
||||
values: [ value(0, '100% 100% 100% 100%',
|
||||
'replace', 'linear'),
|
||||
value(1, '100% 100% 100% 100%', 'replace') ] },
|
||||
{ property: 'border-image-source',
|
||||
values: [ value(0, 'none', 'replace', 'linear'),
|
||||
value(1, 'none', 'replace') ] },
|
||||
{ property: 'border-image-width',
|
||||
values: [ value(0, '1 1 1 1', 'replace', 'linear'),
|
||||
value(1, '1 1 1 1', 'replace') ] },
|
||||
{ property: '-moz-border-bottom-colors',
|
||||
values: [ value(0, 'none', 'replace', 'linear'),
|
||||
value(1, 'none', 'replace') ] },
|
||||
{ property: '-moz-border-left-colors',
|
||||
values: [ value(0, 'none', 'replace', 'linear'),
|
||||
value(1, 'none', 'replace') ] },
|
||||
{ property: '-moz-border-right-colors',
|
||||
values: [ value(0, 'none', 'replace', 'linear'),
|
||||
value(1, 'none', 'replace') ] },
|
||||
{ property: '-moz-border-top-colors',
|
||||
values: [ value(0, 'none', 'replace', 'linear'),
|
||||
value(1, 'none', 'replace') ] } ]
|
||||
},
|
||||
{ desc: 'a keyframe sequence where greater shorthand precedes'
|
||||
+ ' lesser shorthand',
|
||||
@ -478,7 +598,47 @@ var gTests = [
|
||||
value(1, '3px', 'replace') ] },
|
||||
{ property: 'border-top-width',
|
||||
values: [ value(0, '2px', 'replace', 'linear'),
|
||||
value(1, '3px', 'replace') ] } ]
|
||||
value(1, '3px', 'replace') ] },
|
||||
{ property: 'border-bottom-style',
|
||||
values: [ value(0, 'dotted', 'replace', 'linear'),
|
||||
value(1, 'dashed', 'replace') ] },
|
||||
{ property: 'border-left-style',
|
||||
values: [ value(0, 'solid', 'replace', 'linear'),
|
||||
value(1, 'dashed', 'replace') ] },
|
||||
{ property: 'border-right-style',
|
||||
values: [ value(0, 'dotted', 'replace', 'linear'),
|
||||
value(1, 'dashed', 'replace') ] },
|
||||
{ property: 'border-top-style',
|
||||
values: [ value(0, 'dotted', 'replace', 'linear'),
|
||||
value(1, 'dashed', 'replace') ] },
|
||||
{ property: 'border-image-outset',
|
||||
values: [ value(0, '0 0 0 0', 'replace', 'linear'),
|
||||
value(1, '0 0 0 0', 'replace') ] },
|
||||
{ property: 'border-image-repeat',
|
||||
values: [ value(0, 'stretch stretch', 'replace', 'linear'),
|
||||
value(1, 'stretch stretch', 'replace') ] },
|
||||
{ property: 'border-image-slice',
|
||||
values: [ value(0, '100% 100% 100% 100%',
|
||||
'replace', 'linear'),
|
||||
value(1, '100% 100% 100% 100%', 'replace') ] },
|
||||
{ property: 'border-image-source',
|
||||
values: [ value(0, 'none', 'replace', 'linear'),
|
||||
value(1, 'none', 'replace') ] },
|
||||
{ property: 'border-image-width',
|
||||
values: [ value(0, '1 1 1 1', 'replace', 'linear'),
|
||||
value(1, '1 1 1 1', 'replace') ] },
|
||||
{ property: '-moz-border-bottom-colors',
|
||||
values: [ value(0, 'none', 'replace', 'linear'),
|
||||
value(1, 'none', 'replace') ] },
|
||||
{ property: '-moz-border-left-colors',
|
||||
values: [ value(0, 'none', 'replace', 'linear'),
|
||||
value(1, 'none', 'replace') ] },
|
||||
{ property: '-moz-border-right-colors',
|
||||
values: [ value(0, 'none', 'replace', 'linear'),
|
||||
value(1, 'none', 'replace') ] },
|
||||
{ property: '-moz-border-top-colors',
|
||||
values: [ value(0, 'none', 'replace', 'linear'),
|
||||
value(1, 'none', 'replace') ] } ]
|
||||
},
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
@ -41,6 +41,7 @@ support-files =
|
||||
mozilla/file_deferred_start.html
|
||||
mozilla/file_disabled_properties.html
|
||||
mozilla/file_disable_animations_api_core.html
|
||||
mozilla/file_discrete-animations.html
|
||||
mozilla/file_document-timeline-origin-time-range.html
|
||||
mozilla/file_hide_and_show.html
|
||||
mozilla/file_partial_keyframes.html
|
||||
@ -95,6 +96,7 @@ support-files =
|
||||
[mozilla/test_deferred_start.html]
|
||||
[mozilla/test_disable_animations_api_core.html]
|
||||
[mozilla/test_disabled_properties.html]
|
||||
[mozilla/test_discrete-animations.html]
|
||||
[mozilla/test_document-timeline-origin-time-range.html]
|
||||
[mozilla/test_hide_and_show.html]
|
||||
[mozilla/test_partial_keyframes.html]
|
||||
|
170
dom/animation/test/mozilla/file_discrete-animations.html
Normal file
170
dom/animation/test/mozilla/file_discrete-animations.html
Normal file
@ -0,0 +1,170 @@
|
||||
<!doctype html>
|
||||
<head>
|
||||
<meta charset=utf-8>
|
||||
<title>Test Mozilla-specific discrete animatable properties</title>
|
||||
<script type="application/javascript" src="../testcommon.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
const gMozillaSpecificProperties = {
|
||||
"-moz-appearance": {
|
||||
// https://drafts.csswg.org/css-align/#propdef-align-content
|
||||
from: "button",
|
||||
to: "none"
|
||||
},
|
||||
"-moz-border-bottom-colors": {
|
||||
from: "rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0)",
|
||||
to: "rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0)"
|
||||
},
|
||||
"-moz-border-left-colors": {
|
||||
from: "rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0)",
|
||||
to: "rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0)"
|
||||
},
|
||||
"-moz-border-right-colors": {
|
||||
from: "rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0)",
|
||||
to: "rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0)"
|
||||
},
|
||||
"-moz-border-top-colors": {
|
||||
from: "rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0)",
|
||||
to: "rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0)"
|
||||
},
|
||||
"-moz-box-align": {
|
||||
// https://developer.mozilla.org/en/docs/Web/CSS/box-align
|
||||
from: "center",
|
||||
to: "stretch"
|
||||
},
|
||||
"-moz-box-direction": {
|
||||
// https://developer.mozilla.org/en/docs/Web/CSS/box-direction
|
||||
from: "reverse",
|
||||
to: "normal"
|
||||
},
|
||||
"-moz-box-ordinal-group": {
|
||||
// https://developer.mozilla.org/en/docs/Web/CSS/box-ordinal-group
|
||||
from: "1",
|
||||
to: "5"
|
||||
},
|
||||
"-moz-box-orient": {
|
||||
// https://www.w3.org/TR/css-flexbox-1/
|
||||
from: "horizontal",
|
||||
to: "vertical"
|
||||
},
|
||||
"-moz-box-pack": {
|
||||
// https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#propdef-box-pack
|
||||
from: "center",
|
||||
to: "end"
|
||||
},
|
||||
"-moz-float-edge": {
|
||||
// https://developer.mozilla.org/en/docs/Web/CSS/-moz-float-edge
|
||||
from: "margin-box",
|
||||
to: "content-box"
|
||||
},
|
||||
"-moz-force-broken-image-icon": {
|
||||
// https://developer.mozilla.org/en/docs/Web/CSS/-moz-force-broken-image-icon
|
||||
from: "1",
|
||||
to: "5"
|
||||
},
|
||||
"image-rendering": {
|
||||
// https://drafts.csswg.org/css-images-3/#propdef-image-rendering
|
||||
from: "-moz-crisp-edges",
|
||||
to: "auto"
|
||||
},
|
||||
"-moz-stack-sizing": {
|
||||
// https://developer.mozilla.org/en/docs/Web/CSS/-moz-stack-sizing
|
||||
from: "ignore",
|
||||
to: "stretch-to-fit"
|
||||
},
|
||||
"-moz-tab-size": {
|
||||
// https://drafts.csswg.org/css-text-3/#propdef-tab-size
|
||||
from: "1",
|
||||
to: "5"
|
||||
},
|
||||
"-moz-text-size-adjust": {
|
||||
// https://drafts.csswg.org/css-size-adjust/#propdef-text-size-adjust
|
||||
from: "none",
|
||||
to: "auto"
|
||||
},
|
||||
"-webkit-text-stroke-width": {
|
||||
// https://compat.spec.whatwg.org/#propdef--webkit-text-stroke-width
|
||||
from: "10px",
|
||||
to: "50px"
|
||||
}
|
||||
}
|
||||
|
||||
for (let property in gMozillaSpecificProperties) {
|
||||
const testData = gMozillaSpecificProperties[property];
|
||||
const from = testData.from;
|
||||
const to = testData.to;
|
||||
const idlName = propertyToIDL(property);
|
||||
const keyframes = {};
|
||||
keyframes[idlName] = [from, to];
|
||||
|
||||
test(t => {
|
||||
const div = addDiv(t);
|
||||
const animation = div.animate(keyframes,
|
||||
{ duration: 1000, fill: "both" });
|
||||
testAnimationSamples(animation, idlName,
|
||||
[{ time: 0, expected: from.toLowerCase() },
|
||||
{ time: 499, expected: from.toLowerCase() },
|
||||
{ time: 500, expected: to.toLowerCase() },
|
||||
{ time: 1000, expected: to.toLowerCase() }]);
|
||||
}, property + " should animate between '"
|
||||
+ from + "' and '" + to + "' with linear easing");
|
||||
|
||||
test(function(t) {
|
||||
// Easing: http://cubic-bezier.com/#.68,0,1,.01
|
||||
// With this curve, we don't reach the 50% point until about 95% of
|
||||
// the time has expired.
|
||||
const div = addDiv(t);
|
||||
const animation = div.animate(keyframes,
|
||||
{ duration: 1000, fill: "both",
|
||||
easing: "cubic-bezier(0.68,0,1,0.01)" });
|
||||
testAnimationSamples(animation, idlName,
|
||||
[{ time: 0, expected: from.toLowerCase() },
|
||||
{ time: 940, expected: from.toLowerCase() },
|
||||
{ time: 960, expected: to.toLowerCase() }]);
|
||||
}, property + " should animate between '"
|
||||
+ from + "' and '" + to + "' with effect easing");
|
||||
|
||||
test(function(t) {
|
||||
// Easing: http://cubic-bezier.com/#.68,0,1,.01
|
||||
// With this curve, we don't reach the 50% point until about 95% of
|
||||
// the time has expired.
|
||||
keyframes.easing = "cubic-bezier(0.68,0,1,0.01)";
|
||||
const div = addDiv(t);
|
||||
const animation = div.animate(keyframes,
|
||||
{ duration: 1000, fill: "both" });
|
||||
testAnimationSamples(animation, idlName,
|
||||
[{ time: 0, expected: from.toLowerCase() },
|
||||
{ time: 940, expected: from.toLowerCase() },
|
||||
{ time: 960, expected: to.toLowerCase() }]);
|
||||
}, property + " should animate between '"
|
||||
+ from + "' and '" + to + "' with keyframe easing");
|
||||
}
|
||||
|
||||
function propertyToIDL(property) {
|
||||
var prefixMatch = property.match(/^-(\w+)-/);
|
||||
if (prefixMatch) {
|
||||
var prefix = prefixMatch[1] === "moz" ? "Moz" : prefixMatch[1];
|
||||
property = prefix + property.substring(prefixMatch[0].length - 1);
|
||||
}
|
||||
// https://drafts.csswg.org/cssom/#css-property-to-idl-attribute
|
||||
return property.replace(/-([a-z])/gi, function(str, group) {
|
||||
return group.toUpperCase();
|
||||
});
|
||||
}
|
||||
|
||||
function testAnimationSamples(animation, idlName, testSamples) {
|
||||
const target = animation.effect.target;
|
||||
testSamples.forEach(testSample => {
|
||||
animation.currentTime = testSample.time;
|
||||
assert_equals(getComputedStyle(target)[idlName], testSample.expected,
|
||||
"The value should be " + testSample.expected +
|
||||
" at " + testSample.time + "ms");
|
||||
});
|
||||
}
|
||||
|
||||
done();
|
||||
</script>
|
||||
</body>
|
18
dom/animation/test/mozilla/test_discrete-animations.html
Normal file
18
dom/animation/test/mozilla/test_discrete-animations.html
Normal file
@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
<meta charset=utf-8>
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<div id="log"></div>
|
||||
<script>
|
||||
'use strict';
|
||||
setup({explicit_done: true});
|
||||
SpecialPowers.pushPrefEnv(
|
||||
{ "set": [
|
||||
["dom.animations-api.core.enabled", true],
|
||||
["layout.css.osx-font-smoothing.enabled", true],
|
||||
["layout.css.prefixes.webkit", true]
|
||||
] },
|
||||
function() {
|
||||
window.open("file_discrete-animations.html");
|
||||
});
|
||||
</script>
|
94
dom/base/Pose.cpp
Normal file
94
dom/base/Pose.cpp
Normal file
@ -0,0 +1,94 @@
|
||||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "mozilla/HoldDropJSObjects.h"
|
||||
#include "mozilla/dom/TypedArray.h"
|
||||
#include "mozilla/dom/PoseBinding.h"
|
||||
#include "mozilla/dom/Pose.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_CLASS(Pose)
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Pose)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
|
||||
tmp->mPosition = nullptr;
|
||||
tmp->mLinearVelocity = nullptr;
|
||||
tmp->mLinearAcceleration = nullptr;
|
||||
tmp->mOrientation = nullptr;
|
||||
tmp->mAngularVelocity = nullptr;
|
||||
tmp->mAngularAcceleration = nullptr;
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Pose)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(Pose)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
|
||||
NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mPosition)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mLinearVelocity)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mLinearAcceleration)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mOrientation)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mAngularVelocity)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mAngularAcceleration)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRACE_END
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(Pose, AddRef)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(Pose, Release)
|
||||
|
||||
|
||||
Pose::Pose(nsISupports* aParent)
|
||||
: mParent(aParent),
|
||||
mPosition(nullptr),
|
||||
mLinearVelocity(nullptr),
|
||||
mLinearAcceleration(nullptr),
|
||||
mOrientation(nullptr),
|
||||
mAngularVelocity(nullptr),
|
||||
mAngularAcceleration(nullptr)
|
||||
{
|
||||
mozilla::HoldJSObjects(this);
|
||||
}
|
||||
|
||||
Pose::~Pose()
|
||||
{
|
||||
mozilla::DropJSObjects(this);
|
||||
}
|
||||
|
||||
nsISupports*
|
||||
Pose::GetParentObject() const
|
||||
{
|
||||
return mParent;
|
||||
}
|
||||
|
||||
void
|
||||
Pose::SetFloat32Array(JSContext* aJSContext, JS::MutableHandle<JSObject*> aRetVal,
|
||||
JS::Heap<JSObject*>& aObj, float* aVal, uint32_t sizeOfVal,
|
||||
bool bCreate, ErrorResult& aRv)
|
||||
{
|
||||
if (bCreate) {
|
||||
aObj = Float32Array::Create(aJSContext, this,
|
||||
sizeOfVal, aVal);
|
||||
if (!aObj) {
|
||||
aRv.NoteJSContextException(aJSContext);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
aRetVal.set(aObj);
|
||||
}
|
||||
|
||||
/* virtual */ JSObject*
|
||||
Pose::WrapObject(JSContext* aJSContext, JS::Handle<JSObject*> aGivenProto)
|
||||
{
|
||||
return PoseBinding::Wrap(aJSContext, this, aGivenProto);
|
||||
}
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
67
dom/base/Pose.h
Normal file
67
dom/base/Pose.h
Normal file
@ -0,0 +1,67 @@
|
||||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#ifndef mozilla_dom_Pose_h
|
||||
#define mozilla_dom_Pose_h
|
||||
|
||||
#include "nsWrapperCache.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
class Pose : public nsWrapperCache
|
||||
{
|
||||
public:
|
||||
explicit Pose(nsISupports* aParent);
|
||||
|
||||
NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(Pose)
|
||||
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(Pose)
|
||||
|
||||
nsISupports* GetParentObject() const;
|
||||
|
||||
virtual void GetPosition(JSContext* aJSContext,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv) = 0;
|
||||
virtual void GetLinearVelocity(JSContext* aJSContext,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv) = 0;
|
||||
virtual void GetLinearAcceleration(JSContext* aJSContext,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv) = 0;
|
||||
virtual void GetOrientation(JSContext* aJSContext,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv) = 0;
|
||||
virtual void GetAngularVelocity(JSContext* aJSContext,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv) = 0;
|
||||
virtual void GetAngularAcceleration(JSContext* aJSContext,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv) = 0;
|
||||
|
||||
virtual JSObject* WrapObject(JSContext* aJSContext,
|
||||
JS::Handle<JSObject*> aGivenProto) override;
|
||||
|
||||
protected:
|
||||
virtual ~Pose();
|
||||
|
||||
void SetFloat32Array(JSContext* aJSContext, JS::MutableHandle<JSObject*> aRetVal,
|
||||
JS::Heap<JSObject*>& aObj, float* aVal, uint32_t sizeOfVal,
|
||||
bool bCreate, ErrorResult& aRv);
|
||||
|
||||
nsCOMPtr<nsISupports> mParent;
|
||||
|
||||
JS::Heap<JSObject*> mPosition;
|
||||
JS::Heap<JSObject*> mLinearVelocity;
|
||||
JS::Heap<JSObject*> mLinearAcceleration;
|
||||
JS::Heap<JSObject*> mOrientation;
|
||||
JS::Heap<JSObject*> mAngularVelocity;
|
||||
JS::Heap<JSObject*> mAngularAcceleration;
|
||||
};
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
|
||||
#endif // mozilla_dom_Pose_h
|
@ -9,7 +9,6 @@
|
||||
#include "nsGlobalWindow.h"
|
||||
#include "nsITimeoutHandler.h"
|
||||
#include "nsITimer.h"
|
||||
#include "nsPIDOMWindow.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
@ -11,11 +11,13 @@
|
||||
#include "mozilla/TimeStamp.h"
|
||||
#include "nsCOMPtr.h"
|
||||
#include "nsCycleCollectionParticipant.h"
|
||||
#include "nsPIDOMWindow.h"
|
||||
|
||||
class nsGlobalWindow;
|
||||
class nsIPrincipal;
|
||||
class nsITimeoutHandler;
|
||||
class nsITimer;
|
||||
class nsIEventTarget;
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
@ -201,6 +201,7 @@ EXPORTS.mozilla.dom += [
|
||||
'NodeInfoInlines.h',
|
||||
'NodeIterator.h',
|
||||
'PartialSHistory.h',
|
||||
'Pose.h',
|
||||
'ProcessGlobal.h',
|
||||
'ResponsiveImageSelector.h',
|
||||
'SameProcessMessageQueue.h',
|
||||
@ -345,6 +346,7 @@ UNIFIED_SOURCES += [
|
||||
'nsXMLContentSerializer.cpp',
|
||||
'nsXMLNameSpaceMap.cpp',
|
||||
'PartialSHistory.cpp',
|
||||
'Pose.cpp',
|
||||
'PostMessageEvent.cpp',
|
||||
'ProcessGlobal.cpp',
|
||||
'ResponsiveImageSelector.cpp',
|
||||
|
@ -21,7 +21,7 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Gamepad)
|
||||
NS_INTERFACE_MAP_ENTRY(nsISupports)
|
||||
NS_INTERFACE_MAP_END
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Gamepad, mParent, mButtons)
|
||||
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Gamepad, mParent, mButtons, mPose)
|
||||
|
||||
void
|
||||
Gamepad::UpdateTimestamp()
|
||||
@ -52,6 +52,7 @@ Gamepad::Gamepad(nsISupports* aParent,
|
||||
mButtons.InsertElementAt(i, new GamepadButton(mParent));
|
||||
}
|
||||
mAxes.InsertElementsAt(0, aNumAxes, 0.0f);
|
||||
mPose = new GamepadPose(aParent);
|
||||
UpdateTimestamp();
|
||||
}
|
||||
|
||||
@ -87,9 +88,17 @@ Gamepad::SetAxis(uint32_t aAxis, double aValue)
|
||||
UpdateTimestamp();
|
||||
}
|
||||
|
||||
void
|
||||
Gamepad::SetPose(const GamepadPoseState& aPose)
|
||||
{
|
||||
mPose->SetPoseState(aPose);
|
||||
}
|
||||
|
||||
void
|
||||
Gamepad::SyncState(Gamepad* aOther)
|
||||
{
|
||||
const char* kGamepadExtEnabledPref = "dom.gamepad.extensions.enabled";
|
||||
|
||||
if (mButtons.Length() != aOther->mButtons.Length() ||
|
||||
mAxes.Length() != aOther->mAxes.Length()) {
|
||||
return;
|
||||
@ -100,6 +109,7 @@ Gamepad::SyncState(Gamepad* aOther)
|
||||
mButtons[i]->SetPressed(aOther->mButtons[i]->Pressed());
|
||||
mButtons[i]->SetValue(aOther->mButtons[i]->Value());
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
for (uint32_t i = 0; i < mAxes.Length(); ++i) {
|
||||
changed = changed || (mAxes[i] != aOther->mAxes[i]);
|
||||
@ -108,6 +118,12 @@ Gamepad::SyncState(Gamepad* aOther)
|
||||
if (changed) {
|
||||
GamepadBinding::ClearCachedAxesValue(this);
|
||||
}
|
||||
|
||||
if (Preferences::GetBool(kGamepadExtEnabledPref)) {
|
||||
MOZ_ASSERT(aOther->GetPose());
|
||||
mPose->SetPoseState(aOther->GetPose()->GetPoseState());
|
||||
}
|
||||
|
||||
UpdateTimestamp();
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@
|
||||
#include "mozilla/ErrorResult.h"
|
||||
#include "mozilla/dom/GamepadBinding.h"
|
||||
#include "mozilla/dom/GamepadButton.h"
|
||||
#include "mozilla/dom/GamepadPose.h"
|
||||
#include "mozilla/dom/Performance.h"
|
||||
#include <stdint.h>
|
||||
#include "nsCOMPtr.h"
|
||||
@ -49,6 +50,7 @@ public:
|
||||
void SetButton(uint32_t aButton, bool aPressed, double aValue);
|
||||
void SetAxis(uint32_t aAxis, double aValue);
|
||||
void SetIndex(uint32_t aIndex);
|
||||
void SetPose(const GamepadPoseState& aPose);
|
||||
|
||||
// Make the state of this gamepad equivalent to other.
|
||||
void SyncState(Gamepad* aOther);
|
||||
@ -99,6 +101,11 @@ public:
|
||||
aAxes = mAxes;
|
||||
}
|
||||
|
||||
GamepadPose* GetPose() const
|
||||
{
|
||||
return mPose;
|
||||
}
|
||||
|
||||
private:
|
||||
virtual ~Gamepad() {}
|
||||
void UpdateTimestamp();
|
||||
@ -118,6 +125,7 @@ protected:
|
||||
nsTArray<RefPtr<GamepadButton>> mButtons;
|
||||
nsTArray<double> mAxes;
|
||||
DOMHighResTimeStamp mTimestamp;
|
||||
RefPtr<GamepadPose> mPose;
|
||||
};
|
||||
|
||||
} // namespace dom
|
||||
|
@ -405,6 +405,49 @@ GamepadManager::FireAxisMoveEvent(EventTarget* aTarget,
|
||||
aTarget->DispatchEvent(event, &defaultActionEnabled);
|
||||
}
|
||||
|
||||
void
|
||||
GamepadManager::NewPoseEvent(uint32_t aIndex, GamepadServiceType aServiceType,
|
||||
const GamepadPoseState& aPose)
|
||||
{
|
||||
if (mShuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t newIndex = GetGamepadIndexWithServiceType(aIndex, aServiceType);
|
||||
|
||||
RefPtr<Gamepad> gamepad = GetGamepad(newIndex);
|
||||
if (!gamepad) {
|
||||
return;
|
||||
}
|
||||
gamepad->SetPose(aPose);
|
||||
|
||||
// Hold on to listeners in a separate array because firing events
|
||||
// can mutate the mListeners array.
|
||||
nsTArray<RefPtr<nsGlobalWindow>> listeners(mListeners);
|
||||
MOZ_ASSERT(!listeners.IsEmpty());
|
||||
|
||||
for (uint32_t i = 0; i < listeners.Length(); i++) {
|
||||
|
||||
MOZ_ASSERT(listeners[i]->IsInnerWindow());
|
||||
|
||||
// Only send events to non-background windows
|
||||
if (!listeners[i]->AsInner()->IsCurrentInnerWindow() ||
|
||||
listeners[i]->GetOuterWindow()->IsBackground()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool firstTime = MaybeWindowHasSeenGamepad(listeners[i], newIndex);
|
||||
|
||||
RefPtr<Gamepad> listenerGamepad = listeners[i]->GetGamepad(newIndex);
|
||||
if (listenerGamepad) {
|
||||
listenerGamepad->SetPose(aPose);
|
||||
if (firstTime) {
|
||||
FireConnectionEvent(listeners[i], listenerGamepad, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
GamepadManager::NewConnectionEvent(uint32_t aIndex, bool aConnected)
|
||||
{
|
||||
@ -611,6 +654,11 @@ GamepadManager::Update(const GamepadChangeEvent& aEvent)
|
||||
NewAxisMoveEvent(a.index(), a.service_type(), a.axis(), a.value());
|
||||
return;
|
||||
}
|
||||
if (aEvent.type() == GamepadChangeEvent::TGamepadPoseInformation) {
|
||||
const GamepadPoseInformation& a = aEvent.get_GamepadPoseInformation();
|
||||
NewPoseEvent(a.index(), a.service_type(), a.pose_state());
|
||||
return;
|
||||
}
|
||||
|
||||
MOZ_CRASH("We shouldn't be here!");
|
||||
|
||||
|
@ -69,6 +69,11 @@ class GamepadManager final : public nsIObserver,
|
||||
void NewAxisMoveEvent(uint32_t aIndex, GamepadServiceType aServiceType,
|
||||
uint32_t aAxis, double aValue);
|
||||
|
||||
// Update the state of |aState| for the gamepad at |aIndex| for all
|
||||
// windows that are listening and visible.
|
||||
void NewPoseEvent(uint32_t aIndex, GamepadServiceType aServiceType,
|
||||
const GamepadPoseState& aState);
|
||||
|
||||
// Synchronize the state of |aGamepad| to match the gamepad stored at |aIndex|
|
||||
void SyncGamepadState(uint32_t aIndex, Gamepad* aGamepad);
|
||||
|
||||
|
120
dom/gamepad/GamepadPose.cpp
Normal file
120
dom/gamepad/GamepadPose.cpp
Normal file
@ -0,0 +1,120 @@
|
||||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "nsWrapperCache.h"
|
||||
|
||||
#include "mozilla/HoldDropJSObjects.h"
|
||||
#include "mozilla/dom/GamepadPoseBinding.h"
|
||||
#include "mozilla/dom/GamepadPose.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
GamepadPose::GamepadPose(nsISupports* aParent, const GamepadPoseState& aState)
|
||||
: Pose(aParent),
|
||||
mPoseState(aState)
|
||||
{
|
||||
mozilla::HoldJSObjects(this);
|
||||
}
|
||||
|
||||
GamepadPose::GamepadPose(nsISupports* aParent)
|
||||
: Pose(aParent)
|
||||
{
|
||||
mozilla::HoldJSObjects(this);
|
||||
mPoseState.Clear();
|
||||
}
|
||||
|
||||
GamepadPose::~GamepadPose()
|
||||
{
|
||||
mozilla::DropJSObjects(this);
|
||||
}
|
||||
|
||||
/* virtual */ JSObject*
|
||||
GamepadPose::WrapObject(JSContext* aJSContext, JS::Handle<JSObject*> aGivenProto)
|
||||
{
|
||||
return GamepadPoseBinding::Wrap(aJSContext, this, aGivenProto);
|
||||
}
|
||||
|
||||
bool
|
||||
GamepadPose::HasOrientation() const
|
||||
{
|
||||
return bool(mPoseState.flags & GamepadCapabilityFlags::Cap_Position);
|
||||
}
|
||||
|
||||
bool
|
||||
GamepadPose::HasPosition() const
|
||||
{
|
||||
return bool(mPoseState.flags & GamepadCapabilityFlags::Cap_Orientation);
|
||||
}
|
||||
|
||||
void
|
||||
GamepadPose::GetPosition(JSContext* aJSContext,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
SetFloat32Array(aJSContext, aRetval, mPosition, mPoseState.position, 3,
|
||||
bool(mPoseState.flags & GamepadCapabilityFlags::Cap_Position), aRv);
|
||||
}
|
||||
|
||||
void
|
||||
GamepadPose::GetLinearVelocity(JSContext* aJSContext,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
SetFloat32Array(aJSContext, aRetval, mLinearVelocity, mPoseState.linearVelocity, 3,
|
||||
bool(mPoseState.flags & GamepadCapabilityFlags::Cap_Position), aRv);
|
||||
}
|
||||
|
||||
void
|
||||
GamepadPose::GetLinearAcceleration(JSContext* aJSContext,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
SetFloat32Array(aJSContext, aRetval, mLinearAcceleration, mPoseState.linearAcceleration, 3,
|
||||
bool(mPoseState.flags & GamepadCapabilityFlags::Cap_LinearAcceleration), aRv);
|
||||
}
|
||||
|
||||
void
|
||||
GamepadPose::GetOrientation(JSContext* aJSContext,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
SetFloat32Array(aJSContext, aRetval, mOrientation, mPoseState.orientation, 4,
|
||||
bool(mPoseState.flags & GamepadCapabilityFlags::Cap_Orientation), aRv);
|
||||
}
|
||||
|
||||
void
|
||||
GamepadPose::GetAngularVelocity(JSContext* aJSContext,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
SetFloat32Array(aJSContext, aRetval, mAngularVelocity, mPoseState.angularVelocity, 3,
|
||||
bool(mPoseState.flags & GamepadCapabilityFlags::Cap_Orientation), aRv);
|
||||
}
|
||||
|
||||
void
|
||||
GamepadPose::GetAngularAcceleration(JSContext* aJSContext,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
SetFloat32Array(aJSContext, aRetval, mAngularAcceleration, mPoseState.angularAcceleration, 3,
|
||||
bool(mPoseState.flags & GamepadCapabilityFlags::Cap_AngularAcceleration), aRv);
|
||||
}
|
||||
|
||||
void
|
||||
GamepadPose::SetPoseState(const GamepadPoseState& aPose)
|
||||
{
|
||||
mPoseState = aPose;
|
||||
}
|
||||
|
||||
const GamepadPoseState&
|
||||
GamepadPose::GetPoseState()
|
||||
{
|
||||
return mPoseState;
|
||||
}
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
58
dom/gamepad/GamepadPose.h
Normal file
58
dom/gamepad/GamepadPose.h
Normal file
@ -0,0 +1,58 @@
|
||||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#ifndef mozilla_dom_gamepad_GamepadPose_h
|
||||
#define mozilla_dom_gamepad_GamepadPose_h
|
||||
|
||||
#include "mozilla/TypedEnumBits.h"
|
||||
#include "mozilla/dom/Pose.h"
|
||||
#include "mozilla/dom/GamepadPoseState.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
class GamepadPose final : public Pose
|
||||
{
|
||||
public:
|
||||
GamepadPose(nsISupports* aParent, const GamepadPoseState& aState);
|
||||
explicit GamepadPose(nsISupports* aParent);
|
||||
|
||||
virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
|
||||
|
||||
bool HasOrientation() const;
|
||||
bool HasPosition() const;
|
||||
virtual void GetPosition(JSContext* aJSContext,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv) override;
|
||||
virtual void GetLinearVelocity(JSContext* aJSContext,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv) override;
|
||||
virtual void GetLinearAcceleration(JSContext* aJSContext,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv) override;
|
||||
virtual void GetOrientation(JSContext* aJSContext,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv) override;
|
||||
virtual void GetAngularVelocity(JSContext* aJSContext,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv) override;
|
||||
virtual void GetAngularAcceleration(JSContext* aJSContext,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv) override;
|
||||
void SetPoseState(const GamepadPoseState& aPose);
|
||||
const GamepadPoseState& GetPoseState();
|
||||
|
||||
private:
|
||||
virtual ~GamepadPose();
|
||||
|
||||
nsCOMPtr<nsISupports> mParent;
|
||||
GamepadPoseState mPoseState;
|
||||
};
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
|
||||
#endif // mozilla_dom_gamepad_GamepadPose_h
|
88
dom/gamepad/GamepadPoseState.h
Normal file
88
dom/gamepad/GamepadPoseState.h
Normal file
@ -0,0 +1,88 @@
|
||||
|
||||
#ifndef mozilla_dom_gamepad_GamepadPoseState_h_
|
||||
#define mozilla_dom_gamepad_GamepadPoseState_h_
|
||||
|
||||
namespace mozilla{
|
||||
namespace dom{
|
||||
|
||||
enum class GamepadCapabilityFlags : uint16_t {
|
||||
Cap_None = 0,
|
||||
/**
|
||||
* Cap_Position is set if the Gamepad is capable of tracking its position.
|
||||
*/
|
||||
Cap_Position = 1 << 1,
|
||||
/**
|
||||
* Cap_Orientation is set if the Gamepad is capable of tracking its orientation.
|
||||
*/
|
||||
Cap_Orientation = 1 << 2,
|
||||
/**
|
||||
* Cap_AngularAcceleration is set if the Gamepad is capable of tracking its
|
||||
* angular acceleration.
|
||||
*/
|
||||
Cap_AngularAcceleration = 1 << 3,
|
||||
/**
|
||||
* Cap_LinearAcceleration is set if the Gamepad is capable of tracking its
|
||||
* linear acceleration.
|
||||
*/
|
||||
Cap_LinearAcceleration = 1 << 4,
|
||||
/**
|
||||
* Cap_All used for validity checking during IPC serialization
|
||||
*/
|
||||
Cap_All = (1 << 5) - 1
|
||||
};
|
||||
|
||||
MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(GamepadCapabilityFlags)
|
||||
|
||||
struct GamepadPoseState
|
||||
{
|
||||
GamepadCapabilityFlags flags;
|
||||
float orientation[4];
|
||||
float position[3];
|
||||
float angularVelocity[3];
|
||||
float angularAcceleration[3];
|
||||
float linearVelocity[3];
|
||||
float linearAcceleration[3];
|
||||
|
||||
GamepadPoseState()
|
||||
{
|
||||
Clear();
|
||||
}
|
||||
|
||||
bool operator==(const GamepadPoseState& aPose) const
|
||||
{
|
||||
return flags == aPose.flags
|
||||
&& orientation[0] == aPose.orientation[0]
|
||||
&& orientation[1] == aPose.orientation[1]
|
||||
&& orientation[2] == aPose.orientation[2]
|
||||
&& orientation[3] == aPose.orientation[3]
|
||||
&& position[0] == aPose.position[0]
|
||||
&& position[1] == aPose.position[1]
|
||||
&& position[2] == aPose.position[2]
|
||||
&& angularVelocity[0] == aPose.angularVelocity[0]
|
||||
&& angularVelocity[1] == aPose.angularVelocity[1]
|
||||
&& angularVelocity[2] == aPose.angularVelocity[2]
|
||||
&& angularAcceleration[0] == aPose.angularAcceleration[0]
|
||||
&& angularAcceleration[1] == aPose.angularAcceleration[1]
|
||||
&& angularAcceleration[2] == aPose.angularAcceleration[2]
|
||||
&& linearVelocity[0] == aPose.linearVelocity[0]
|
||||
&& linearVelocity[1] == aPose.linearVelocity[1]
|
||||
&& linearVelocity[2] == aPose.linearVelocity[2]
|
||||
&& linearAcceleration[0] == aPose.linearAcceleration[0]
|
||||
&& linearAcceleration[1] == aPose.linearAcceleration[1]
|
||||
&& linearAcceleration[2] == aPose.linearAcceleration[2];
|
||||
}
|
||||
|
||||
bool operator!=(const GamepadPoseState& aPose) const
|
||||
{
|
||||
return !(*this == aPose);
|
||||
}
|
||||
|
||||
void Clear() {
|
||||
memset(this, 0, sizeof(GamepadPoseState));
|
||||
}
|
||||
};
|
||||
|
||||
}// namespace dom
|
||||
}// namespace mozilla
|
||||
|
||||
#endif // mozilla_dom_gamepad_GamepadPoseState_h_
|
@ -3,6 +3,7 @@
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
using mozilla::dom::GamepadServiceType from "mozilla/dom/GamepadMessageUtils.h";
|
||||
using mozilla::dom::GamepadPoseState from "mozilla/dom/GamepadMessageUtils.h";
|
||||
|
||||
|
||||
namespace mozilla {
|
||||
@ -40,11 +41,18 @@ struct GamepadButtonInformation {
|
||||
double value;
|
||||
};
|
||||
|
||||
struct GamepadPoseInformation {
|
||||
uint32_t index;
|
||||
GamepadServiceType service_type;
|
||||
GamepadPoseState pose_state;
|
||||
};
|
||||
|
||||
union GamepadChangeEvent {
|
||||
GamepadAdded;
|
||||
GamepadRemoved;
|
||||
GamepadAxisInformation;
|
||||
GamepadButtonInformation;
|
||||
GamepadPoseInformation;
|
||||
};
|
||||
|
||||
} // namespace dom
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
#include "ipc/IPCMessageUtils.h"
|
||||
#include "mozilla/dom/GamepadServiceType.h"
|
||||
#include "mozilla/dom/GamepadPoseState.h"
|
||||
|
||||
namespace IPC {
|
||||
|
||||
@ -13,6 +14,69 @@ struct ParamTraits<mozilla::dom::GamepadServiceType> :
|
||||
mozilla::dom::GamepadServiceType(0),
|
||||
mozilla::dom::GamepadServiceType(
|
||||
mozilla::dom::GamepadServiceType::NumGamepadServiceType)> {};
|
||||
|
||||
template<>
|
||||
struct ParamTraits<mozilla::dom::GamepadCapabilityFlags> :
|
||||
public BitFlagsEnumSerializer<mozilla::dom::GamepadCapabilityFlags,
|
||||
mozilla::dom::GamepadCapabilityFlags::Cap_All> {};
|
||||
|
||||
template <>
|
||||
struct ParamTraits<mozilla::dom::GamepadPoseState>
|
||||
{
|
||||
typedef mozilla::dom::GamepadPoseState paramType;
|
||||
|
||||
static void Write(Message* aMsg, const paramType& aParam)
|
||||
{
|
||||
WriteParam(aMsg, aParam.flags);
|
||||
WriteParam(aMsg, aParam.orientation[0]);
|
||||
WriteParam(aMsg, aParam.orientation[1]);
|
||||
WriteParam(aMsg, aParam.orientation[2]);
|
||||
WriteParam(aMsg, aParam.orientation[3]);
|
||||
WriteParam(aMsg, aParam.position[0]);
|
||||
WriteParam(aMsg, aParam.position[1]);
|
||||
WriteParam(aMsg, aParam.position[2]);
|
||||
WriteParam(aMsg, aParam.angularVelocity[0]);
|
||||
WriteParam(aMsg, aParam.angularVelocity[1]);
|
||||
WriteParam(aMsg, aParam.angularVelocity[2]);
|
||||
WriteParam(aMsg, aParam.angularAcceleration[0]);
|
||||
WriteParam(aMsg, aParam.angularAcceleration[1]);
|
||||
WriteParam(aMsg, aParam.angularAcceleration[2]);
|
||||
WriteParam(aMsg, aParam.linearVelocity[0]);
|
||||
WriteParam(aMsg, aParam.linearVelocity[1]);
|
||||
WriteParam(aMsg, aParam.linearVelocity[2]);
|
||||
WriteParam(aMsg, aParam.linearAcceleration[0]);
|
||||
WriteParam(aMsg, aParam.linearAcceleration[1]);
|
||||
WriteParam(aMsg, aParam.linearAcceleration[2]);
|
||||
}
|
||||
|
||||
static bool Read(const Message* aMsg, PickleIterator* aIter, paramType* aResult)
|
||||
{
|
||||
if (!ReadParam(aMsg, aIter, &(aResult->flags)) ||
|
||||
!ReadParam(aMsg, aIter, &(aResult->orientation[0])) ||
|
||||
!ReadParam(aMsg, aIter, &(aResult->orientation[1])) ||
|
||||
!ReadParam(aMsg, aIter, &(aResult->orientation[2])) ||
|
||||
!ReadParam(aMsg, aIter, &(aResult->orientation[3])) ||
|
||||
!ReadParam(aMsg, aIter, &(aResult->position[0])) ||
|
||||
!ReadParam(aMsg, aIter, &(aResult->position[1])) ||
|
||||
!ReadParam(aMsg, aIter, &(aResult->position[2])) ||
|
||||
!ReadParam(aMsg, aIter, &(aResult->angularVelocity[0])) ||
|
||||
!ReadParam(aMsg, aIter, &(aResult->angularVelocity[1])) ||
|
||||
!ReadParam(aMsg, aIter, &(aResult->angularVelocity[2])) ||
|
||||
!ReadParam(aMsg, aIter, &(aResult->angularAcceleration[0])) ||
|
||||
!ReadParam(aMsg, aIter, &(aResult->angularAcceleration[1])) ||
|
||||
!ReadParam(aMsg, aIter, &(aResult->angularAcceleration[2])) ||
|
||||
!ReadParam(aMsg, aIter, &(aResult->linearVelocity[0])) ||
|
||||
!ReadParam(aMsg, aIter, &(aResult->linearVelocity[1])) ||
|
||||
!ReadParam(aMsg, aIter, &(aResult->linearVelocity[2])) ||
|
||||
!ReadParam(aMsg, aIter, &(aResult->linearAcceleration[0])) ||
|
||||
!ReadParam(aMsg, aIter, &(aResult->linearAcceleration[1])) ||
|
||||
!ReadParam(aMsg, aIter, &(aResult->linearAcceleration[2]))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace IPC
|
||||
|
||||
#endif // mozilla_dom_gamepad_GamepadMessageUtils_h
|
@ -11,6 +11,7 @@ IPDL_SOURCES += [
|
||||
]
|
||||
|
||||
EXPORTS.mozilla.dom += [
|
||||
'GamepadPoseState.h',
|
||||
'ipc/GamepadMessageUtils.h',
|
||||
'ipc/GamepadServiceType.h'
|
||||
]
|
||||
@ -22,6 +23,7 @@ if CONFIG['MOZ_GAMEPAD']:
|
||||
'GamepadManager.h',
|
||||
'GamepadMonitoring.h',
|
||||
'GamepadPlatformService.h',
|
||||
'GamepadPose.h',
|
||||
'GamepadServiceTest.h',
|
||||
'ipc/GamepadEventChannelChild.h',
|
||||
'ipc/GamepadEventChannelParent.h',
|
||||
@ -35,6 +37,7 @@ if CONFIG['MOZ_GAMEPAD']:
|
||||
'GamepadManager.cpp',
|
||||
'GamepadMonitoring.cpp',
|
||||
'GamepadPlatformService.cpp',
|
||||
'GamepadPose.cpp',
|
||||
'GamepadServiceTest.cpp',
|
||||
'ipc/GamepadEventChannelChild.cpp',
|
||||
'ipc/GamepadEventChannelParent.cpp',
|
||||
|
@ -404,6 +404,8 @@ var interfaceNamesInGlobalScope =
|
||||
"GamepadButton",
|
||||
// IMPORTANT: Do not change this list without review from a DOM peer!
|
||||
"GamepadEvent",
|
||||
// IMPORTANT: Do not change this list without review from a DOM peer!
|
||||
{name: "GamepadPose", release: false},
|
||||
// IMPORTANT: Do not change this list without review from a DOM peer!
|
||||
"HashChangeEvent",
|
||||
// IMPORTANT: Do not change this list without review from a DOM peer!
|
||||
@ -774,6 +776,8 @@ var interfaceNamesInGlobalScope =
|
||||
"PopupBlockedEvent",
|
||||
// IMPORTANT: Do not change this list without review from a DOM peer!
|
||||
{name: "PopupBoxObject", xbl: true},
|
||||
// IMPORTANT: Do not change this list without review from a DOM peer!
|
||||
{name: "Pose", release: false},
|
||||
// IMPORTANT: Do not change this list without review from a DOM peer!
|
||||
{name: "PresentationDeviceInfoManager",
|
||||
disabled: true},
|
||||
|
@ -256,59 +256,16 @@ VRDisplayCapabilities::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenPr
|
||||
return VRDisplayCapabilitiesBinding::Wrap(aCx, this, aGivenProto);
|
||||
}
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_CLASS(VRPose)
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(VRPose)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
|
||||
tmp->mPosition = nullptr;
|
||||
tmp->mLinearVelocity = nullptr;
|
||||
tmp->mLinearAcceleration = nullptr;
|
||||
tmp->mOrientation = nullptr;
|
||||
tmp->mAngularVelocity = nullptr;
|
||||
tmp->mAngularAcceleration = nullptr;
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(VRPose)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(VRPose)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
|
||||
NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mPosition)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mLinearVelocity)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mLinearAcceleration)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mOrientation)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mAngularVelocity)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mAngularAcceleration)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRACE_END
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(VRPose, AddRef)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(VRPose, Release)
|
||||
|
||||
VRPose::VRPose(nsISupports* aParent, const gfx::VRHMDSensorState& aState)
|
||||
: mParent(aParent)
|
||||
: Pose(aParent)
|
||||
, mVRState(aState)
|
||||
, mPosition(nullptr)
|
||||
, mLinearVelocity(nullptr)
|
||||
, mLinearAcceleration(nullptr)
|
||||
, mOrientation(nullptr)
|
||||
, mAngularVelocity(nullptr)
|
||||
, mAngularAcceleration(nullptr)
|
||||
{
|
||||
mFrameId = aState.inputFrameID;
|
||||
mozilla::HoldJSObjects(this);
|
||||
}
|
||||
|
||||
VRPose::VRPose(nsISupports* aParent)
|
||||
: mParent(aParent)
|
||||
, mPosition(nullptr)
|
||||
, mLinearVelocity(nullptr)
|
||||
, mLinearAcceleration(nullptr)
|
||||
, mOrientation(nullptr)
|
||||
, mAngularVelocity(nullptr)
|
||||
, mAngularAcceleration(nullptr)
|
||||
: Pose(aParent)
|
||||
{
|
||||
mFrameId = 0;
|
||||
mVRState.Clear();
|
||||
@ -325,15 +282,9 @@ VRPose::GetPosition(JSContext* aCx,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
if (!mPosition && mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_Position) {
|
||||
// Lazily create the Float32Array
|
||||
mPosition = dom::Float32Array::Create(aCx, this, 3, mVRState.position);
|
||||
if (!mPosition) {
|
||||
aRv.NoteJSContextException(aCx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
aRetval.set(mPosition);
|
||||
SetFloat32Array(aCx, aRetval, mPosition, mVRState.position, 3,
|
||||
!mPosition && bool(mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_Position),
|
||||
aRv);
|
||||
}
|
||||
|
||||
void
|
||||
@ -341,15 +292,9 @@ VRPose::GetLinearVelocity(JSContext* aCx,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
if (!mLinearVelocity && mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_Position) {
|
||||
// Lazily create the Float32Array
|
||||
mLinearVelocity = dom::Float32Array::Create(aCx, this, 3, mVRState.linearVelocity);
|
||||
if (!mLinearVelocity) {
|
||||
aRv.NoteJSContextException(aCx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
aRetval.set(mLinearVelocity);
|
||||
SetFloat32Array(aCx, aRetval, mLinearVelocity, mVRState.linearVelocity, 3,
|
||||
!mLinearVelocity && bool(mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_Position),
|
||||
aRv);
|
||||
}
|
||||
|
||||
void
|
||||
@ -357,15 +302,10 @@ VRPose::GetLinearAcceleration(JSContext* aCx,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
if (!mLinearAcceleration && mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_LinearAcceleration) {
|
||||
// Lazily create the Float32Array
|
||||
mLinearAcceleration = dom::Float32Array::Create(aCx, this, 3, mVRState.linearAcceleration);
|
||||
if (!mLinearAcceleration) {
|
||||
aRv.NoteJSContextException(aCx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
aRetval.set(mLinearAcceleration);
|
||||
SetFloat32Array(aCx, aRetval, mLinearAcceleration, mVRState.linearAcceleration, 3,
|
||||
!mLinearAcceleration && bool(mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_LinearAcceleration),
|
||||
aRv);
|
||||
|
||||
}
|
||||
|
||||
void
|
||||
@ -373,15 +313,9 @@ VRPose::GetOrientation(JSContext* aCx,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
if (!mOrientation && mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_Orientation) {
|
||||
// Lazily create the Float32Array
|
||||
mOrientation = dom::Float32Array::Create(aCx, this, 4, mVRState.orientation);
|
||||
if (!mOrientation) {
|
||||
aRv.NoteJSContextException(aCx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
aRetval.set(mOrientation);
|
||||
SetFloat32Array(aCx, aRetval, mOrientation, mVRState.orientation, 4,
|
||||
!mOrientation && bool(mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_Orientation),
|
||||
aRv);
|
||||
}
|
||||
|
||||
void
|
||||
@ -389,15 +323,9 @@ VRPose::GetAngularVelocity(JSContext* aCx,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
if (!mAngularVelocity && mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_Orientation) {
|
||||
// Lazily create the Float32Array
|
||||
mAngularVelocity = dom::Float32Array::Create(aCx, this, 3, mVRState.angularVelocity);
|
||||
if (!mAngularVelocity) {
|
||||
aRv.NoteJSContextException(aCx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
aRetval.set(mAngularVelocity);
|
||||
SetFloat32Array(aCx, aRetval, mAngularVelocity, mVRState.angularVelocity, 3,
|
||||
!mAngularVelocity && bool(mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_Orientation),
|
||||
aRv);
|
||||
}
|
||||
|
||||
void
|
||||
@ -405,15 +333,9 @@ VRPose::GetAngularAcceleration(JSContext* aCx,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
if (!mAngularAcceleration && mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_AngularAcceleration) {
|
||||
// Lazily create the Float32Array
|
||||
mAngularAcceleration = dom::Float32Array::Create(aCx, this, 3, mVRState.angularAcceleration);
|
||||
if (!mAngularAcceleration) {
|
||||
aRv.NoteJSContextException(aCx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
aRetval.set(mAngularAcceleration);
|
||||
SetFloat32Array(aCx, aRetval, mAngularAcceleration, mVRState.angularAcceleration, 3,
|
||||
!mAngularAcceleration && bool(mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_AngularAcceleration),
|
||||
aRv);
|
||||
}
|
||||
|
||||
JSObject*
|
||||
|
@ -15,11 +15,11 @@
|
||||
#include "mozilla/DOMEventTargetHelper.h"
|
||||
#include "mozilla/dom/DOMPoint.h"
|
||||
#include "mozilla/dom/DOMRect.h"
|
||||
#include "mozilla/dom/Pose.h"
|
||||
|
||||
#include "nsCOMPtr.h"
|
||||
#include "nsString.h"
|
||||
#include "nsTArray.h"
|
||||
#include "nsWrapperCache.h"
|
||||
|
||||
#include "gfxVR.h"
|
||||
|
||||
@ -95,54 +95,41 @@ protected:
|
||||
gfx::VRDisplayCapabilityFlags mFlags;
|
||||
};
|
||||
|
||||
class VRPose final : public nsWrapperCache
|
||||
class VRPose final : public Pose
|
||||
{
|
||||
|
||||
public:
|
||||
VRPose(nsISupports* aParent, const gfx::VRHMDSensorState& aState);
|
||||
explicit VRPose(nsISupports* aParent);
|
||||
|
||||
NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(VRPose)
|
||||
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(VRPose)
|
||||
|
||||
uint32_t FrameID() const { return mFrameId; }
|
||||
|
||||
void GetPosition(JSContext* aCx,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv);
|
||||
void GetLinearVelocity(JSContext* aCx,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv);
|
||||
void GetLinearAcceleration(JSContext* aCx,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv);
|
||||
void GetOrientation(JSContext* aCx,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv);
|
||||
void GetAngularVelocity(JSContext* aCx,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv);
|
||||
void GetAngularAcceleration(JSContext* aCx,
|
||||
virtual void GetPosition(JSContext* aCx,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv) override;
|
||||
virtual void GetLinearVelocity(JSContext* aCx,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv) override;
|
||||
virtual void GetLinearAcceleration(JSContext* aCx,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv) override;
|
||||
virtual void GetOrientation(JSContext* aCx,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv);
|
||||
ErrorResult& aRv) override;
|
||||
virtual void GetAngularVelocity(JSContext* aCx,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv) override;
|
||||
virtual void GetAngularAcceleration(JSContext* aCx,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv) override;
|
||||
|
||||
nsISupports* GetParentObject() const { return mParent; }
|
||||
virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
|
||||
|
||||
protected:
|
||||
~VRPose();
|
||||
nsCOMPtr<nsISupports> mParent;
|
||||
|
||||
uint32_t mFrameId;
|
||||
gfx::VRHMDSensorState mVRState;
|
||||
|
||||
JS::Heap<JSObject*> mPosition;
|
||||
JS::Heap<JSObject*> mLinearVelocity;
|
||||
JS::Heap<JSObject*> mLinearAcceleration;
|
||||
JS::Heap<JSObject*> mOrientation;
|
||||
JS::Heap<JSObject*> mAngularVelocity;
|
||||
JS::Heap<JSObject*> mAngularAcceleration;
|
||||
|
||||
};
|
||||
|
||||
struct VRFrameInfo
|
||||
|
@ -56,4 +56,10 @@ interface Gamepad {
|
||||
* Timestamp from when the data of this device was last updated.
|
||||
*/
|
||||
readonly attribute DOMHighResTimeStamp timestamp;
|
||||
|
||||
/**
|
||||
* The current pose of the device, a GamepadPose.
|
||||
*/
|
||||
[Pref="dom.gamepad.extensions.enabled"]
|
||||
readonly attribute GamepadPose? pose;
|
||||
};
|
||||
|
13
dom/webidl/GamepadPose.webidl
Normal file
13
dom/webidl/GamepadPose.webidl
Normal file
@ -0,0 +1,13 @@
|
||||
/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
[Pref="dom.gamepad.extensions.enabled",
|
||||
HeaderFile="mozilla/dom/GamepadPose.h"]
|
||||
interface GamepadPose : Pose
|
||||
{
|
||||
readonly attribute boolean hasOrientation;
|
||||
readonly attribute boolean hasPosition;
|
||||
};
|
23
dom/webidl/Pose.webidl
Normal file
23
dom/webidl/Pose.webidl
Normal file
@ -0,0 +1,23 @@
|
||||
/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
interface Pose
|
||||
{
|
||||
/**
|
||||
* position, linearVelocity, and linearAcceleration are 3-component vectors.
|
||||
* position is relative to a sitting space. Transforming this point with
|
||||
* VRStageParameters.sittingToStandingTransform converts this to standing space.
|
||||
*/
|
||||
[Constant, Throws] readonly attribute Float32Array? position;
|
||||
[Constant, Throws] readonly attribute Float32Array? linearVelocity;
|
||||
[Constant, Throws] readonly attribute Float32Array? linearAcceleration;
|
||||
|
||||
/* orientation is a 4-entry array representing the components of a quaternion. */
|
||||
[Constant, Throws] readonly attribute Float32Array? orientation;
|
||||
/* angularVelocity and angularAcceleration are the components of 3-dimensional vectors. */
|
||||
[Constant, Throws] readonly attribute Float32Array? angularVelocity;
|
||||
[Constant, Throws] readonly attribute Float32Array? angularAcceleration;
|
||||
};
|
@ -116,21 +116,9 @@ interface VRStageParameters {
|
||||
|
||||
[Pref="dom.vr.enabled",
|
||||
HeaderFile="mozilla/dom/VRDisplay.h"]
|
||||
interface VRPose {
|
||||
/**
|
||||
* position, linearVelocity, and linearAcceleration are 3-component vectors.
|
||||
* position is relative to a sitting space. Transforming this point with
|
||||
* VRStageParameters.sittingToStandingTransform converts this to standing space.
|
||||
*/
|
||||
[Constant, Throws] readonly attribute Float32Array? position;
|
||||
[Constant, Throws] readonly attribute Float32Array? linearVelocity;
|
||||
[Constant, Throws] readonly attribute Float32Array? linearAcceleration;
|
||||
interface VRPose : Pose
|
||||
{
|
||||
|
||||
/* orientation is a 4-entry array representing the components of a quaternion. */
|
||||
[Constant, Throws] readonly attribute Float32Array? orientation;
|
||||
/* angularVelocity and angularAcceleration are the components of 3-dimensional vectors. */
|
||||
[Constant, Throws] readonly attribute Float32Array? angularVelocity;
|
||||
[Constant, Throws] readonly attribute Float32Array? angularAcceleration;
|
||||
};
|
||||
|
||||
[Constructor,
|
||||
|
@ -367,6 +367,7 @@ WEBIDL_FILES = [
|
||||
'PluginArray.webidl',
|
||||
'PointerEvent.webidl',
|
||||
'PopupBoxObject.webidl',
|
||||
'Pose.webidl',
|
||||
'Position.webidl',
|
||||
'PositionError.webidl',
|
||||
'Presentation.webidl',
|
||||
@ -647,6 +648,7 @@ if CONFIG['MOZ_WEBSPEECH']:
|
||||
if CONFIG['MOZ_GAMEPAD']:
|
||||
WEBIDL_FILES += [
|
||||
'Gamepad.webidl',
|
||||
'GamepadPose.webidl',
|
||||
'GamepadServiceTest.webidl'
|
||||
]
|
||||
|
||||
|
@ -1956,6 +1956,19 @@ HTMLEditRules::WillDeleteSelection(Selection* aSelection,
|
||||
DeprecatedAbs(eo - so));
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
// XXX When Backspace key is pressed, Chromium removes following empty
|
||||
// text nodes when removing the last character of the non-empty text
|
||||
// node. However, Edge never removes empty text nodes even if
|
||||
// selection is in the following empty text node(s). For now, we
|
||||
// should keep our traditional behavior same as Edge for backward
|
||||
// compatibility.
|
||||
// XXX When Delete key is pressed, Edge removes all preceding empty
|
||||
// text nodes when removing the first character of the non-empty
|
||||
// text node. Chromium removes only selected empty text node and
|
||||
// following empty text nodes and the first character of the
|
||||
// non-empty text node. For now, we should keep our traditional
|
||||
// behavior same as Chromium for backward compatibility.
|
||||
|
||||
DeleteNodeIfCollapsedText(nodeAsText);
|
||||
|
||||
rv = InsertBRIfNeeded(aSelection);
|
||||
|
@ -483,14 +483,12 @@ WSRunObject::PriorVisibleNode(nsINode* aNode,
|
||||
for (; run; run = run->mLeft) {
|
||||
if (run->mType == WSType::normalWS) {
|
||||
WSPoint point = GetCharBefore(aNode, aOffset);
|
||||
if (point.mTextNode) {
|
||||
// When it's a non-empty text node, return it.
|
||||
if (point.mTextNode && point.mTextNode->Length()) {
|
||||
*outVisNode = point.mTextNode;
|
||||
*outVisOffset = point.mOffset + 1;
|
||||
if (nsCRT::IsAsciiSpace(point.mChar) || point.mChar == nbsp) {
|
||||
*outType = WSType::normalWS;
|
||||
} else if (!point.mChar) {
|
||||
// MOOSE: not possible?
|
||||
*outType = WSType::none;
|
||||
} else {
|
||||
*outType = WSType::text;
|
||||
}
|
||||
@ -527,14 +525,12 @@ WSRunObject::NextVisibleNode(nsINode* aNode,
|
||||
for (; run; run = run->mRight) {
|
||||
if (run->mType == WSType::normalWS) {
|
||||
WSPoint point = GetCharAfter(aNode, aOffset);
|
||||
if (point.mTextNode) {
|
||||
// When it's a non-empty text node, return it.
|
||||
if (point.mTextNode && point.mTextNode->Length()) {
|
||||
*outVisNode = point.mTextNode;
|
||||
*outVisOffset = point.mOffset;
|
||||
if (nsCRT::IsAsciiSpace(point.mChar) || point.mChar == nbsp) {
|
||||
*outType = WSType::normalWS;
|
||||
} else if (!point.mChar) {
|
||||
// MOOSE: not possible?
|
||||
*outType = WSType::none;
|
||||
} else {
|
||||
*outType = WSType::text;
|
||||
}
|
||||
|
@ -210,6 +210,7 @@ skip-if = toolkit == 'android'
|
||||
[test_bug1248185.html]
|
||||
[test_bug1258085.html]
|
||||
[test_bug1268736.html]
|
||||
[test_bug1315065.html]
|
||||
|
||||
[test_CF_HTML_clipboard.html]
|
||||
subsuite = clipboard
|
||||
|
145
editor/libeditor/tests/test_bug1315065.html
Normal file
145
editor/libeditor/tests/test_bug1315065.html
Normal file
@ -0,0 +1,145 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
https://bugzilla.mozilla.org/show_bug.cgi?id=1315065
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test for Bug 1315065</title>
|
||||
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1315065">Mozilla Bug 1315065</a>
|
||||
<div contenteditable><p>abc<br></p></div>
|
||||
<script type="application/javascript">
|
||||
/** Test for Bug 1315065 **/
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
SimpleTest.waitForFocus(()=>{
|
||||
var editor = document.getElementsByTagName("div")[0];
|
||||
function initForBackspace(aSelectionCollapsedTo /* = 0 ~ 3 */) {
|
||||
editor.innerHTML = "<p id='p'>abc<br></p>";
|
||||
var p = document.getElementById("p");
|
||||
// FYI: We cannot inserting empty text nodes as expected with
|
||||
// Node.appendChild() nor Node.insertBefore(). Therefore, let's use
|
||||
// Range.insertNode() like actual web apps.
|
||||
var selection = window.getSelection();
|
||||
selection.collapse(p, 1);
|
||||
var range = selection.getRangeAt(0);
|
||||
var emptyTextNode3 = document.createTextNode("");
|
||||
range.insertNode(emptyTextNode3);
|
||||
var emptyTextNode2 = document.createTextNode("");
|
||||
range.insertNode(emptyTextNode2);
|
||||
var emptyTextNode1 = document.createTextNode("");
|
||||
range.insertNode(emptyTextNode1);
|
||||
is(p.childNodes.length, 5, "Failed to initialize the editor");
|
||||
is(p.childNodes.item(1), emptyTextNode1, "1st text node should be emptyTextNode1");
|
||||
is(p.childNodes.item(2), emptyTextNode2, "2nd text node should be emptyTextNode2");
|
||||
is(p.childNodes.item(3), emptyTextNode3, "3rd text node should be emptyTextNode3");
|
||||
switch (aSelectionCollapsedTo) {
|
||||
case 0:
|
||||
selection.collapse(p.firstChild, 3); // next to 'c'
|
||||
break;
|
||||
case 1:
|
||||
selection.collapse(emptyTextNode1, 0);
|
||||
break;
|
||||
case 2:
|
||||
selection.collapse(emptyTextNode2, 0);
|
||||
break;
|
||||
case 3:
|
||||
selection.collapse(emptyTextNode3, 0);
|
||||
break;
|
||||
default:
|
||||
ok(false, "aSelectionCollapsedTo is illegal value");
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < 4; i++) {
|
||||
const kDescription = i == 0 ? "Backspace from immediately after the last character" :
|
||||
"Backspace from " + i + "th empty text node";
|
||||
editor.focus();
|
||||
initForBackspace(i);
|
||||
synthesizeKey("KEY_Backspace", { code: "Backspace" });
|
||||
var p = document.getElementById("p");
|
||||
ok(p, kDescription + ": <p> element shouldn't be removed by Backspace key press");
|
||||
is(p.tagName.toLowerCase(), "p", kDescription + ": <p> element shouldn't be removed by Backspace key press");
|
||||
// When Backspace key is pressed even in empty text nodes, Gecko should not remove empty text nodes for now
|
||||
// because we should keep our traditional behavior (same as Edge) for backward compatibility as far as possible.
|
||||
// In this case, Chromium removes all empty text nodes, but Edge doesn't remove any empty text nodes.
|
||||
is(p.childNodes.length, 5, kDescription + ": <p> should have 5 children after pressing Backspace key");
|
||||
is(p.childNodes.item(0).textContent, "ab", kDescription + ": 'c' should be removed by pressing Backspace key");
|
||||
is(p.childNodes.item(1).textContent, "", kDescription + ": 1st empty text node should not be removed by pressing Backspace key");
|
||||
is(p.childNodes.item(2).textContent, "", kDescription + ": 2nd empty text node should not be removed by pressing Backspace key");
|
||||
is(p.childNodes.item(3).textContent, "", kDescription + ": 3rd empty text node should not be removed by pressing Backspace key");
|
||||
editor.blur();
|
||||
}
|
||||
|
||||
function initForDelete(aSelectionCollapsedTo /* = 0 ~ 3 */) {
|
||||
editor.innerHTML = "<p id='p'>abc<br></p>";
|
||||
var p = document.getElementById("p");
|
||||
// FYI: We cannot inserting empty text nodes as expected with
|
||||
// Node.appendChild() nor Node.insertBefore(). Therefore, let's use
|
||||
// Range.insertNode() like actual web apps.
|
||||
var selection = window.getSelection();
|
||||
selection.collapse(p, 0);
|
||||
var range = selection.getRangeAt(0);
|
||||
var emptyTextNode1 = document.createTextNode("");
|
||||
range.insertNode(emptyTextNode1);
|
||||
var emptyTextNode2 = document.createTextNode("");
|
||||
range.insertNode(emptyTextNode2);
|
||||
var emptyTextNode3 = document.createTextNode("");
|
||||
range.insertNode(emptyTextNode3);
|
||||
is(p.childNodes.length, 5, "Failed to initialize the editor");
|
||||
is(p.childNodes.item(0), emptyTextNode3, "1st text node should be emptyTextNode3");
|
||||
is(p.childNodes.item(1), emptyTextNode2, "2nd text node should be emptyTextNode2");
|
||||
is(p.childNodes.item(2), emptyTextNode1, "3rd text node should be emptyTextNode1");
|
||||
switch (aSelectionCollapsedTo) {
|
||||
case 0:
|
||||
selection.collapse(p.childNodes.item(3), 0); // next to 'a'
|
||||
break;
|
||||
case 1:
|
||||
selection.collapse(emptyTextNode1, 0);
|
||||
break;
|
||||
case 2:
|
||||
selection.collapse(emptyTextNode2, 0);
|
||||
break;
|
||||
case 3:
|
||||
selection.collapse(emptyTextNode3, 0);
|
||||
break;
|
||||
default:
|
||||
ok(false, "aSelectionCollapsedTo is illegal value");
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < 4; i++) {
|
||||
const kDescription = i == 0 ? "Delete from immediately before the first character" :
|
||||
"Delete from " + i + "th empty text node";
|
||||
editor.focus();
|
||||
initForDelete(i);
|
||||
synthesizeKey("KEY_Delete", { code: "Delete" });
|
||||
var p = document.getElementById("p");
|
||||
ok(p, kDescription + ": <p> element shouldn't be removed by Delete key press");
|
||||
is(p.tagName.toLowerCase(), "p", kDescription + ": <p> element shouldn't be removed by Delete key press");
|
||||
if (i == 0) {
|
||||
// If Delete key is pressed in non-empty text node, only the text node should be modified.
|
||||
// This is same behavior as Chromium, but different from Edge. Edge removes all empty text nodes in this case.
|
||||
is(p.childNodes.length, 5, kDescription + ": <p> should have only 2 children after pressing Delete key (empty text nodes should be removed");
|
||||
is(p.childNodes.item(0).textContent, "", kDescription + ": 1st empty text node should not be removed by pressing Delete key");
|
||||
is(p.childNodes.item(1).textContent, "", kDescription + ": 2nd empty text node should not be removed by pressing Delete key");
|
||||
is(p.childNodes.item(2).textContent, "", kDescription + ": 3rd empty text node should not be removed by pressing Delete key");
|
||||
is(p.childNodes.item(3).textContent, "bc", kDescription + ": 'a' should be removed by pressing Delete key");
|
||||
} else {
|
||||
// If Delete key is pressed in an empty text node, it and following empty text nodes should be removed and the non-empty text node should be modified.
|
||||
// This is same behavior as Chromium, but different from Edge. Edge removes all empty text nodes in this case.
|
||||
var expectedEmptyTextNodes = 3 - i;
|
||||
is(p.childNodes.length, expectedEmptyTextNodes + 2, kDescription + ": <p> should have only " + i + " children after pressing Delete key (" + i + " empty text nodes should be removed");
|
||||
is(p.childNodes.item(expectedEmptyTextNodes).textContent, "bc", kDescription + ": empty text nodes and 'a' should be removed by pressing Delete key");
|
||||
}
|
||||
editor.blur();
|
||||
}
|
||||
SimpleTest.finish();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -186,3 +186,16 @@ VRControllerHost::GetButtonPressed()
|
||||
{
|
||||
return mButtonPressed;
|
||||
}
|
||||
|
||||
void
|
||||
VRControllerHost::SetPose(const dom::GamepadPoseState& aPose)
|
||||
{
|
||||
mPose = aPose;
|
||||
}
|
||||
|
||||
const dom::GamepadPoseState&
|
||||
VRControllerHost::GetPose()
|
||||
{
|
||||
return mPose;
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@
|
||||
#include "mozilla/EnumeratedArray.h"
|
||||
#include "mozilla/TimeStamp.h"
|
||||
#include "mozilla/TypedEnumBits.h"
|
||||
#include "mozilla/dom/GamepadPoseState.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace layers {
|
||||
@ -92,6 +93,8 @@ public:
|
||||
uint32_t GetIndex();
|
||||
void SetButtonPressed(uint64_t aBit);
|
||||
uint64_t GetButtonPressed();
|
||||
void SetPose(const dom::GamepadPoseState& aPose);
|
||||
const dom::GamepadPoseState& GetPose();
|
||||
|
||||
protected:
|
||||
explicit VRControllerHost(VRDeviceType aType);
|
||||
@ -102,6 +105,7 @@ protected:
|
||||
uint32_t mIndex;
|
||||
// The current button pressed bit of button mask.
|
||||
uint64_t mButtonPressed;
|
||||
dom::GamepadPoseState mPose;
|
||||
};
|
||||
|
||||
} // namespace gfx
|
||||
|
@ -94,6 +94,10 @@ VRManager::VRManager()
|
||||
mManagers.AppendElement(mgr);
|
||||
}
|
||||
#endif
|
||||
// Enable gamepad extensions while VR is enabled.
|
||||
if (gfxPrefs::VREnabled()) {
|
||||
Preferences::SetBool("dom.gamepad.extensions.enabled", true);
|
||||
}
|
||||
}
|
||||
|
||||
VRManager::~VRManager()
|
||||
|
@ -113,3 +113,15 @@ VRControllerManager::NewAxisMove(uint32_t aIndex, uint32_t aAxis,
|
||||
MOZ_ASSERT(vm);
|
||||
vm->NotifyGamepadChange<dom::GamepadAxisInformation>(a);
|
||||
}
|
||||
|
||||
void
|
||||
VRControllerManager::NewPoseState(uint32_t aIndex,
|
||||
const dom::GamepadPoseState& aPose)
|
||||
{
|
||||
dom::GamepadPoseInformation a(aIndex, dom::GamepadServiceType::VR,
|
||||
aPose);
|
||||
|
||||
VRManager* vm = VRManager::Get();
|
||||
MOZ_ASSERT(vm);
|
||||
vm->NotifyGamepadChange<dom::GamepadPoseInformation>(a);
|
||||
}
|
||||
|
@ -20,6 +20,10 @@ namespace mozilla {
|
||||
namespace layers {
|
||||
class PTextureParent;
|
||||
}
|
||||
namespace dom {
|
||||
enum class GamepadMappingType : uint32_t;
|
||||
struct GamepadPoseState;
|
||||
}
|
||||
namespace gfx {
|
||||
class VRLayerParent;
|
||||
class VRDisplayHost;
|
||||
@ -252,6 +256,7 @@ public:
|
||||
virtual void ScanForDevices() = 0;
|
||||
void NewButtonEvent(uint32_t aIndex, uint32_t aButton, bool aPressed);
|
||||
void NewAxisMove(uint32_t aIndex, uint32_t aAxis, double aValue);
|
||||
void NewPoseState(uint32_t aIndex, const dom::GamepadPoseState& aPose);
|
||||
void AddGamepad(const char* aID, uint32_t aMapping,
|
||||
uint32_t aNumButtons, uint32_t aNumAxes);
|
||||
void RemoveGamepad(uint32_t aIndex);
|
||||
@ -269,6 +274,9 @@ private:
|
||||
uint64_t aButtonPressed) = 0;
|
||||
virtual void HandleAxisMove(uint32_t aControllerIdx, uint32_t aAxis,
|
||||
float aValue) = 0;
|
||||
virtual void HandlePoseTracking(uint32_t aControllerIdx,
|
||||
const dom::GamepadPoseState& aPose,
|
||||
VRControllerHost* aController) = 0;
|
||||
};
|
||||
|
||||
} // namespace gfx
|
||||
|
@ -490,7 +490,7 @@ VRControllerOpenVR::VRControllerOpenVR()
|
||||
MOZ_COUNT_CTOR_INHERITED(VRControllerOpenVR, VRControllerHost);
|
||||
mControllerInfo.mControllerName.AssignLiteral("OpenVR HMD");
|
||||
#ifdef MOZ_GAMEPAD
|
||||
mControllerInfo.mMappingType = static_cast<uint32_t>(dom::GamepadMappingType::_empty);
|
||||
mControllerInfo.mMappingType = static_cast<uint32_t>(GamepadMappingType::_empty);
|
||||
#else
|
||||
mControllerInfo.mMappingType = 0;
|
||||
#endif
|
||||
@ -583,6 +583,9 @@ VRControllerManagerOpenVR::HandleInput()
|
||||
|
||||
MOZ_ASSERT(mVRSystem);
|
||||
|
||||
vr::TrackedDevicePose_t poses[vr::k_unMaxTrackedDeviceCount];
|
||||
mVRSystem->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseSeated, 0.0f,
|
||||
poses, vr::k_unMaxTrackedDeviceCount);
|
||||
// Process OpenVR controller state
|
||||
for (uint32_t i = 0; i < mOpenVRController.Length(); ++i) {
|
||||
controller = mOpenVRController[i];
|
||||
@ -605,6 +608,44 @@ VRControllerManagerOpenVR::HandleInput()
|
||||
HandleAxisMove(controller->GetIndex(), axis,
|
||||
state.rAxis[gOpenVRAxes[axis]].x);
|
||||
}
|
||||
|
||||
// Start to process pose
|
||||
const ::vr::TrackedDevicePose_t& pose = poses[controller->GetTrackedIndex()];
|
||||
|
||||
if (pose.bDeviceIsConnected && pose.bPoseIsValid &&
|
||||
pose.eTrackingResult == vr::TrackingResult_Running_OK) {
|
||||
gfx::Matrix4x4 m;
|
||||
|
||||
// NOTE! mDeviceToAbsoluteTracking is a 3x4 matrix, not 4x4. But
|
||||
// because of its arrangement, we can copy the 12 elements in and
|
||||
// then transpose them to the right place. We do this so we can
|
||||
// pull out a Quaternion.
|
||||
memcpy(&m.components, &pose.mDeviceToAbsoluteTracking, sizeof(float) * 12);
|
||||
m.Transpose();
|
||||
|
||||
gfx::Quaternion rot;
|
||||
rot.SetFromRotationMatrix(m);
|
||||
rot.Invert();
|
||||
|
||||
GamepadPoseState poseState;
|
||||
poseState.flags |= GamepadCapabilityFlags::Cap_Orientation;
|
||||
poseState.orientation[0] = rot.x;
|
||||
poseState.orientation[1] = rot.y;
|
||||
poseState.orientation[2] = rot.z;
|
||||
poseState.orientation[3] = rot.w;
|
||||
poseState.angularVelocity[0] = pose.vAngularVelocity.v[0];
|
||||
poseState.angularVelocity[1] = pose.vAngularVelocity.v[1];
|
||||
poseState.angularVelocity[2] = pose.vAngularVelocity.v[2];
|
||||
|
||||
poseState.flags |= GamepadCapabilityFlags::Cap_Position;
|
||||
poseState.position[0] = m._41;
|
||||
poseState.position[1] = m._42;
|
||||
poseState.position[2] = m._43;
|
||||
poseState.linearVelocity[0] = pose.vVelocity.v[0];
|
||||
poseState.linearVelocity[1] = pose.vVelocity.v[1];
|
||||
poseState.linearVelocity[2] = pose.vVelocity.v[2];
|
||||
HandlePoseTracking(controller->GetIndex(), poseState, controller);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -644,6 +685,17 @@ VRControllerManagerOpenVR::HandleAxisMove(uint32_t aControllerIdx, uint32_t aAxi
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
VRControllerManagerOpenVR::HandlePoseTracking(uint32_t aControllerIdx,
|
||||
const GamepadPoseState& aPose,
|
||||
VRControllerHost* aController)
|
||||
{
|
||||
if (aPose != aController->GetPose()) {
|
||||
aController->SetPose(aPose);
|
||||
NewPoseState(aControllerIdx, aPose);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
VRControllerManagerOpenVR::GetControllers(nsTArray<RefPtr<VRControllerHost>>& aControllerResult)
|
||||
{
|
||||
|
@ -124,6 +124,9 @@ private:
|
||||
uint64_t aButtonPressed) override;
|
||||
virtual void HandleAxisMove(uint32_t aControllerIdx, uint32_t aAxis,
|
||||
float aValue) override;
|
||||
virtual void HandlePoseTracking(uint32_t aControllerIdx,
|
||||
const dom::GamepadPoseState& aPose,
|
||||
VRControllerHost* aController) override;
|
||||
|
||||
bool mOpenVRInstalled;
|
||||
nsTArray<RefPtr<impl::VRControllerOpenVR>> mOpenVRController;
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -50,7 +50,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=435442
|
||||
25% { margin-top: 100px }
|
||||
}
|
||||
@keyframes kf4 {
|
||||
to, from { border-collapse: collapse; margin-top: 37px }
|
||||
to, from { display: none; margin-top: 37px }
|
||||
}
|
||||
@keyframes kf_cascade1 {
|
||||
from { padding-top: 50px }
|
||||
@ -566,23 +566,23 @@ done_div();
|
||||
// we still override the value when two consecutive keyframes have
|
||||
// the same value.
|
||||
new_div("animation: kf4 ease 10s");
|
||||
is(cs.borderCollapse, "separate",
|
||||
is(cs.display, "block",
|
||||
"non-animatable properties should be ignored (linear, 0s)");
|
||||
is(cs.marginTop, "37px",
|
||||
"animatable properties should still apply (linear, 0s)");
|
||||
advance_clock(1000);
|
||||
is(cs.borderCollapse, "separate",
|
||||
is(cs.display, "block",
|
||||
"non-animatable properties should be ignored (linear, 1s)");
|
||||
is(cs.marginTop, "37px",
|
||||
"animatable properties should still apply (linear, 1s)");
|
||||
done_div();
|
||||
new_div("animation: kf4 step-start 10s");
|
||||
is(cs.borderCollapse, "separate",
|
||||
is(cs.display, "block",
|
||||
"non-animatable properties should be ignored (step-start, 0s)");
|
||||
is(cs.marginTop, "37px",
|
||||
"animatable properties should still apply (step-start, 0s)");
|
||||
advance_clock(1000);
|
||||
is(cs.borderCollapse, "separate",
|
||||
is(cs.display, "block",
|
||||
"non-animatable properties should be ignored (step-start, 1s)");
|
||||
is(cs.marginTop, "37px",
|
||||
"animatable properties should still apply (step-start, 1s)");
|
||||
|
@ -61,7 +61,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=964646
|
||||
25% { transform: translate(100px) }
|
||||
}
|
||||
@keyframes kf4 {
|
||||
to, from { border-collapse: collapse; transform: translate(37px) }
|
||||
to, from { display: none; transform: translate(37px) }
|
||||
}
|
||||
@keyframes kf_cascade1 {
|
||||
from { transform: translate(50px) }
|
||||
@ -634,12 +634,12 @@ addAsyncAnimTest(function *() {
|
||||
new_div("animation: kf4 ease 10s");
|
||||
yield waitForPaintsFlushed();
|
||||
var cs = window.getComputedStyle(gDiv);
|
||||
is(cs.borderCollapse, "separate",
|
||||
is(cs.display, "block",
|
||||
"non-animatable properties should be ignored (linear, 0s)");
|
||||
omta_is("transform", { tx: 37 }, RunningOn.Compositor,
|
||||
"animatable properties should still apply (linear, 0s)");
|
||||
advance_clock(1000);
|
||||
is(cs.borderCollapse, "separate",
|
||||
is(cs.display, "block",
|
||||
"non-animatable properties should be ignored (linear, 1s)");
|
||||
omta_is("transform", { tx: 37 }, RunningOn.Compositor,
|
||||
"animatable properties should still apply (linear, 1s)");
|
||||
@ -647,12 +647,12 @@ addAsyncAnimTest(function *() {
|
||||
new_div("animation: kf4 step-start 10s");
|
||||
yield waitForPaintsFlushed();
|
||||
cs = window.getComputedStyle(gDiv);
|
||||
is(cs.borderCollapse, "separate",
|
||||
is(cs.display, "block",
|
||||
"non-animatable properties should be ignored (step-start, 0s)");
|
||||
omta_is("transform", { tx: 37 }, RunningOn.Compositor,
|
||||
"animatable properties should still apply (step-start, 0s)");
|
||||
advance_clock(1000);
|
||||
is(cs.borderCollapse, "separate",
|
||||
is(cs.display, "block",
|
||||
"non-animatable properties should be ignored (step-start, 1s)");
|
||||
omta_is("transform", { tx: 37 }, RunningOn.Compositor,
|
||||
"animatable properties should still apply (step-start, 1s)");
|
||||
|
@ -203,6 +203,7 @@ pref("dom.gamepad.non_standard_events.enabled", false);
|
||||
#else
|
||||
pref("dom.gamepad.non_standard_events.enabled", true);
|
||||
#endif
|
||||
pref("dom.gamepad.extensions.enabled", false);
|
||||
|
||||
// Whether the KeyboardEvent.code is enabled
|
||||
pref("dom.keyboardevent.code.enabled", true);
|
||||
|
@ -1,3 +1,7 @@
|
||||
prefs: [layout.css.contain.enabled:true,
|
||||
layout.css.initial-letter.enabled:true,
|
||||
layout.css.overflow-clip-box.enabled:true,
|
||||
layout.css.shape-outside.enabled:true]
|
||||
[type-per-property.html]
|
||||
type: testharness
|
||||
[flex-basis supports animating as combination units 'px' and '%']
|
||||
@ -31,4 +35,3 @@
|
||||
[text-combine-upright uses discrete animation when animating between 'all' and 'digits' with keyframe easing]
|
||||
expected: FAIL
|
||||
bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1258635
|
||||
|
||||
|
@ -34,12 +34,144 @@ var gCSSProperties = {
|
||||
discrete("flex-start", "flex-end")
|
||||
]
|
||||
},
|
||||
"backface-visibility": {
|
||||
// https://drafts.csswg.org/css-transforms/#propdef-backface-visibility
|
||||
"tests": [
|
||||
discrete("visible", "hidden")
|
||||
]
|
||||
},
|
||||
"background-attachment": {
|
||||
// https://drafts.csswg.org/css-backgrounds-3/#background-attachment
|
||||
"tests": [
|
||||
discrete("fixed", "local")
|
||||
]
|
||||
},
|
||||
"background-blend-mode": {
|
||||
// https://drafts.fxtf.org/compositing-1/#propdef-background-blend-mode
|
||||
"tests": [
|
||||
discrete("multiply", "screen")
|
||||
]
|
||||
},
|
||||
"background-clip": {
|
||||
// https://drafts.csswg.org/css-backgrounds-3/#background-clip
|
||||
"tests": [
|
||||
discrete("padding-box", "content-box")
|
||||
]
|
||||
},
|
||||
"background-image": {
|
||||
// https://drafts.csswg.org/css-backgrounds-3/#background-image
|
||||
"tests": [
|
||||
discrete("url(\"http://localhost/test-1\")", "url(\"http://localhost/test-2\")")
|
||||
]
|
||||
},
|
||||
"background-origin": {
|
||||
// https://drafts.csswg.org/css-backgrounds-3/#background-origin
|
||||
"tests": [
|
||||
discrete("padding-box", "content-box")
|
||||
]
|
||||
},
|
||||
"background-repeat": {
|
||||
// https://drafts.csswg.org/css-backgrounds-3/#background-repeat
|
||||
"tests": [
|
||||
discrete("space", "round")
|
||||
]
|
||||
},
|
||||
"border-bottom-style": {
|
||||
// https://drafts.csswg.org/css-backgrounds-3/#border-bottom-style
|
||||
"tests": [
|
||||
discrete("dotted", "solid")
|
||||
]
|
||||
},
|
||||
"border-collapse": {
|
||||
// https://drafts.csswg.org/css-tables/#propdef-border-collapse
|
||||
"tests": [
|
||||
discrete("collapse", "separate")
|
||||
]
|
||||
},
|
||||
"border-image-outset": {
|
||||
// https://drafts.csswg.org/css-backgrounds-3/#border-image-outset
|
||||
"tests": [
|
||||
discrete("1 1 1 1", "5 5 5 5")
|
||||
]
|
||||
},
|
||||
"border-image-repeat": {
|
||||
// https://drafts.csswg.org/css-backgrounds-3/#border-image-repeat
|
||||
"tests": [
|
||||
discrete("stretch stretch", "repeat repeat")
|
||||
]
|
||||
},
|
||||
"border-image-slice": {
|
||||
// https://drafts.csswg.org/css-backgrounds-3/#border-image-slice
|
||||
"tests": [
|
||||
discrete("1 1 1 1", "5 5 5 5")
|
||||
]
|
||||
},
|
||||
"border-image-source": {
|
||||
// https://drafts.csswg.org/css-backgrounds-3/#border-image-source
|
||||
"tests": [
|
||||
discrete("url(\"http://localhost/test-1\")", "url(\"http://localhost/test-2\")")
|
||||
]
|
||||
},
|
||||
"border-image-width": {
|
||||
// https://drafts.csswg.org/css-backgrounds-3/#border-image-width
|
||||
"tests": [
|
||||
discrete("1 1 1 1", "5 5 5 5")
|
||||
]
|
||||
},
|
||||
"border-left-style": {
|
||||
// https://drafts.csswg.org/css-backgrounds-3/#border-left-style
|
||||
"tests": [
|
||||
discrete("dotted", "solid")
|
||||
]
|
||||
},
|
||||
"border-right-style": {
|
||||
// https://drafts.csswg.org/css-backgrounds-3/#border-right-style
|
||||
"tests": [
|
||||
discrete("dotted", "solid")
|
||||
]
|
||||
},
|
||||
"border-top-style": {
|
||||
// https://drafts.csswg.org/css-backgrounds-3/#border-top-style
|
||||
"tests": [
|
||||
discrete("dotted", "solid")
|
||||
]
|
||||
},
|
||||
"box-decoration-break": {
|
||||
// https://drafts.csswg.org/css-break/#propdef-box-decoration-break
|
||||
"tests": [
|
||||
discrete("slice", "clone")
|
||||
]
|
||||
},
|
||||
"box-sizing": {
|
||||
// https://drafts.csswg.org/css-ui-4/#box-sizing
|
||||
"tests": [
|
||||
discrete("content-box", "border-box")
|
||||
]
|
||||
},
|
||||
"caption-side": {
|
||||
// https://drafts.csswg.org/css-tables/#propdef-caption-side
|
||||
"tests": [
|
||||
discrete("top", "bottom")
|
||||
]
|
||||
},
|
||||
"clear": {
|
||||
// https://drafts.csswg.org/css-page-floats/#propdef-clear
|
||||
"tests": [
|
||||
discrete("inline-start", "inline-end")
|
||||
]
|
||||
},
|
||||
"clip-rule": {
|
||||
// https://drafts.fxtf.org/css-masking-1/#propdef-clip-rule
|
||||
tests: [
|
||||
discrete("evenodd", "nonzero")
|
||||
]
|
||||
},
|
||||
"color-adjust": {
|
||||
// https://drafts.csswg.org/css-color-4/#color-adjust
|
||||
tests: [
|
||||
discrete("economy", "exact")
|
||||
]
|
||||
},
|
||||
"color-interpolation": {
|
||||
// https://svgwg.org/svg2-draft/painting.html#ColorInterpolationProperty
|
||||
tests: [
|
||||
@ -52,12 +184,67 @@ var gCSSProperties = {
|
||||
discrete("sRGB", "linearRGB")
|
||||
]
|
||||
},
|
||||
"column-fill": {
|
||||
// https://drafts.csswg.org/css-multicol/#propdef-column-fill
|
||||
tests: [
|
||||
discrete("auto", "balance")
|
||||
]
|
||||
},
|
||||
"column-rule-style": {
|
||||
// https://drafts.csswg.org/css-multicol/#propdef-column-rule-style
|
||||
tests: [
|
||||
discrete("none", "dotted")
|
||||
]
|
||||
},
|
||||
"contain": {
|
||||
// https://drafts.csswg.org/css-containment/#propdef-contain
|
||||
tests: [
|
||||
discrete("strict", "none")
|
||||
]
|
||||
},
|
||||
"content": {
|
||||
// https://drafts.csswg.org/css-content-3/#propdef-content
|
||||
tests: [
|
||||
discrete("\"a\"", "\"b\"")
|
||||
],
|
||||
tagName: "::before"
|
||||
},
|
||||
"counter-increment": {
|
||||
// https://drafts.csswg.org/css-lists-3/#propdef-counter-increment
|
||||
tests: [
|
||||
discrete("ident-1 1", "ident-2 2")
|
||||
]
|
||||
},
|
||||
"counter-reset": {
|
||||
// https://drafts.csswg.org/css-lists-3/#propdef-counter-reset
|
||||
tests: [
|
||||
discrete("ident-1 1", "ident-2 2")
|
||||
]
|
||||
},
|
||||
"cursor": {
|
||||
// https://drafts.csswg.org/css2/ui.html#propdef-cursor
|
||||
tests: [
|
||||
discrete("pointer", "wait")
|
||||
]
|
||||
},
|
||||
"direction": {
|
||||
// https://drafts.csswg.org/css-writing-modes-3/#propdef-direction
|
||||
tests: [
|
||||
discrete("ltr", "rtl")
|
||||
]
|
||||
},
|
||||
"dominant-baseline": {
|
||||
// https://drafts.csswg.org/css-inline/#propdef-dominant-baseline
|
||||
tests: [
|
||||
discrete("ideographic", "alphabetic")
|
||||
]
|
||||
},
|
||||
"empty-cells": {
|
||||
// https://drafts.csswg.org/css-tables/#propdef-empty-cells
|
||||
tests: [
|
||||
discrete("show", "hide")
|
||||
]
|
||||
},
|
||||
"fill-rule": {
|
||||
// https://svgwg.org/svg2-draft/painting.html#FillRuleProperty
|
||||
tests: [
|
||||
@ -101,10 +288,172 @@ var gCSSProperties = {
|
||||
discrete("italic", "oblique")
|
||||
]
|
||||
},
|
||||
"image-rendering": {
|
||||
// https://drafts.csswg.org/css-images/#propdef-image-rendering
|
||||
"float": {
|
||||
// https://drafts.csswg.org/css-page-floats/#propdef-float
|
||||
tests: [
|
||||
discrete("optimizeQuality", "pixelated")
|
||||
discrete("left", "right")
|
||||
]
|
||||
},
|
||||
"font-family": {
|
||||
// https://drafts.csswg.org/css-fonts-3/#propdef-font-family
|
||||
tests: [
|
||||
discrete("helvetica", "verdana")
|
||||
]
|
||||
},
|
||||
"font-feature-settings": {
|
||||
// https://drafts.csswg.org/css-fonts/#descdef-font-feature-settings
|
||||
tests: [
|
||||
discrete("\"liga\" 5", "normal")
|
||||
]
|
||||
},
|
||||
"font-kerning": {
|
||||
// https://drafts.csswg.org/css-fonts-3/#propdef-font-kerning
|
||||
tests: [
|
||||
discrete("auto", "normal")
|
||||
]
|
||||
},
|
||||
"font-language-override": {
|
||||
// https://drafts.csswg.org/css-fonts-3/#propdef-font-language-override
|
||||
tests: [
|
||||
discrete("\"eng\"", "normal")
|
||||
]
|
||||
},
|
||||
"font-style": {
|
||||
// https://drafts.csswg.org/css-fonts-3/#propdef-font-style
|
||||
tests: [
|
||||
discrete("italic", "oblique")
|
||||
]
|
||||
},
|
||||
"font-synthesis": {
|
||||
// https://drafts.csswg.org/css-fonts-3/#propdef-font-synthesis
|
||||
tests: [
|
||||
discrete("none", "weight style")
|
||||
]
|
||||
},
|
||||
"font-variant-alternates": {
|
||||
// https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-alternates
|
||||
tests: [
|
||||
discrete("swash(unknown)", "stylistic(unknown)")
|
||||
]
|
||||
},
|
||||
"font-variant-caps": {
|
||||
// https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-caps
|
||||
tests: [
|
||||
discrete("small-caps", "unicase")
|
||||
]
|
||||
},
|
||||
"font-variant-east-asian": {
|
||||
// https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-east-asian
|
||||
tests: [
|
||||
discrete("full-width", "proportional-width")
|
||||
]
|
||||
},
|
||||
"font-variant-ligatures": {
|
||||
// https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-ligatures
|
||||
tests: [
|
||||
discrete("common-ligatures", "no-common-ligatures")
|
||||
]
|
||||
},
|
||||
"font-variant-numeric": {
|
||||
// https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-numeric
|
||||
tests: [
|
||||
discrete("lining-nums", "oldstyle-nums")
|
||||
]
|
||||
},
|
||||
"font-variant-position": {
|
||||
// https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-position
|
||||
tests: [
|
||||
discrete("sub", "super")
|
||||
]
|
||||
},
|
||||
"grid-auto-columns": {
|
||||
// https://drafts.csswg.org/css-grid/#propdef-grid-auto-columns
|
||||
tests: [
|
||||
discrete("1px", "5px")
|
||||
]
|
||||
},
|
||||
"grid-auto-flow": {
|
||||
// https://drafts.csswg.org/css-grid/#propdef-grid-auto-flow
|
||||
tests: [
|
||||
discrete("row", "column")
|
||||
]
|
||||
},
|
||||
"grid-auto-rows": {
|
||||
// https://drafts.csswg.org/css-grid/#propdef-grid-auto-rows
|
||||
tests: [
|
||||
discrete("1px", "5px")
|
||||
]
|
||||
},
|
||||
"grid-column-end": {
|
||||
// https://drafts.csswg.org/css-grid/#propdef-grid-column-end
|
||||
tests: [
|
||||
discrete("1", "5")
|
||||
]
|
||||
},
|
||||
"grid-column-start": {
|
||||
// https://drafts.csswg.org/css-grid/#propdef-grid-column-start
|
||||
tests: [
|
||||
discrete("1", "5")
|
||||
]
|
||||
},
|
||||
"grid-row-end": {
|
||||
// https://drafts.csswg.org/css-grid/#propdef-grid-row-end
|
||||
tests: [
|
||||
discrete("1", "5")
|
||||
]
|
||||
},
|
||||
"grid-row-start": {
|
||||
// https://drafts.csswg.org/css-grid/#propdef-grid-row-start
|
||||
tests: [
|
||||
discrete("1", "5")
|
||||
]
|
||||
},
|
||||
"grid-template-areas": {
|
||||
// https://drafts.csswg.org/css-template/#grid-template-areas
|
||||
tests: [
|
||||
discrete("\". . a b\" \". .a b\"", "none")
|
||||
]
|
||||
},
|
||||
"grid-template-columns": {
|
||||
// https://drafts.csswg.org/css-template/#grid-template-columns
|
||||
tests: [
|
||||
discrete("1px", "5px")
|
||||
]
|
||||
},
|
||||
"grid-template-rows": {
|
||||
// https://drafts.csswg.org/css-template/#grid-template-rows
|
||||
tests: [
|
||||
discrete("1px", "5px")
|
||||
]
|
||||
},
|
||||
"hyphens": {
|
||||
// https://drafts.csswg.org/css-text-3/#propdef-hyphens
|
||||
tests: [
|
||||
discrete("manual", "auto")
|
||||
]
|
||||
},
|
||||
"image-orientation": {
|
||||
// https://drafts.csswg.org/css-images-3/#propdef-image-orientation
|
||||
tests: [
|
||||
discrete("0deg", "90deg")
|
||||
]
|
||||
},
|
||||
"ime-mode": {
|
||||
// https://drafts.csswg.org/css-ui/#input-method-editor
|
||||
tests: [
|
||||
discrete("disabled", "auto")
|
||||
]
|
||||
},
|
||||
"initial-letter": {
|
||||
// https://drafts.csswg.org/css-inline/#propdef-initial-letter
|
||||
tests: [
|
||||
discrete("1 2", "3 4")
|
||||
]
|
||||
},
|
||||
"isolation": {
|
||||
// https://drafts.fxtf.org/compositing-1/#propdef-isolation
|
||||
tests: [
|
||||
discrete("auto", "isolate")
|
||||
]
|
||||
},
|
||||
"justify-content": {
|
||||
@ -125,24 +474,186 @@ var gCSSProperties = {
|
||||
discrete("baseline", "last baseline")
|
||||
]
|
||||
},
|
||||
"list-style-image": {
|
||||
// https://drafts.csswg.org/css-lists-3/#propdef-list-style-image
|
||||
tests: [
|
||||
discrete("url(\"http://localhost/test-1\")", "url(\"http://localhost/test-2\")")
|
||||
]
|
||||
},
|
||||
"list-style-position": {
|
||||
// https://drafts.csswg.org/css-lists-3/#propdef-list-style-position
|
||||
tests: [
|
||||
discrete("inside", "outside")
|
||||
]
|
||||
},
|
||||
"list-style-type": {
|
||||
// https://drafts.csswg.org/css-lists-3/#propdef-list-style-type
|
||||
tests: [
|
||||
discrete("circle", "square")
|
||||
]
|
||||
},
|
||||
"marker-end": {
|
||||
// https://svgwg.org/specs/markers/#MarkerEndProperty
|
||||
tests: [
|
||||
discrete("url(\"http://localhost/test-1\")", "url(\"http://localhost/test-2\")")
|
||||
]
|
||||
},
|
||||
"marker-mid": {
|
||||
// https://svgwg.org/specs/markers/#MarkerMidProperty
|
||||
tests: [
|
||||
discrete("url(\"http://localhost/test-1\")", "url(\"http://localhost/test-2\")")
|
||||
]
|
||||
},
|
||||
"marker-start": {
|
||||
// https://svgwg.org/specs/markers/#MarkerStartProperty
|
||||
tests: [
|
||||
discrete("url(\"http://localhost/test-1\")", "url(\"http://localhost/test-2\")")
|
||||
]
|
||||
},
|
||||
"mask": {
|
||||
// https://drafts.fxtf.org/css-masking-1/#the-mask
|
||||
tests: [
|
||||
discrete("url(\"http://localhost/test-1\")", "url(\"http://localhost/test-2\")")
|
||||
]
|
||||
},
|
||||
"mask-clip": {
|
||||
// https://drafts.fxtf.org/css-masking-1/#propdef-mask-clip
|
||||
tests: [
|
||||
discrete("content-box", "border-box")
|
||||
]
|
||||
},
|
||||
"mask-composite": {
|
||||
// https://drafts.fxtf.org/css-masking-1/#propdef-mask-composite
|
||||
tests: [
|
||||
discrete("add", "subtract")
|
||||
]
|
||||
},
|
||||
"mask-image": {
|
||||
// https://drafts.fxtf.org/css-masking-1/#propdef-mask-image
|
||||
tests: [
|
||||
discrete("url(\"http://localhost/test-1\")", "url(\"http://localhost/test-2\")")
|
||||
]
|
||||
},
|
||||
"mask-mode": {
|
||||
// https://drafts.fxtf.org/css-masking-1/#propdef-mask-mode
|
||||
tests: [
|
||||
discrete("alpha", "luminance")
|
||||
]
|
||||
},
|
||||
"mask-origin": {
|
||||
// https://drafts.fxtf.org/css-masking-1/#propdef-mask-origin
|
||||
tests: [
|
||||
discrete("content-box", "border-box")
|
||||
]
|
||||
},
|
||||
"mask-repeat": {
|
||||
// https://drafts.fxtf.org/css-masking-1/#propdef-mask-repeat
|
||||
tests: [
|
||||
discrete("space", "round")
|
||||
]
|
||||
},
|
||||
"mask-type": {
|
||||
// https://drafts.fxtf.org/css-masking-1/#propdef-mask-type
|
||||
tests: [
|
||||
discrete("alpha", "luminance")
|
||||
]
|
||||
},
|
||||
"mix-blend-mode": {
|
||||
// https://drafts.fxtf.org/compositing-1/#propdef-mix-blend-mode
|
||||
tests: [
|
||||
discrete("multiply", "screen")
|
||||
]
|
||||
},
|
||||
"object-fit": {
|
||||
// https://drafts.csswg.org/css-images-3/#propdef-object-fit
|
||||
tests: [
|
||||
discrete("fill", "contain")
|
||||
]
|
||||
},
|
||||
"order": {
|
||||
// https://drafts.csswg.org/css-flexbox/#propdef-order
|
||||
tests: [
|
||||
integer()
|
||||
]
|
||||
},
|
||||
"outline-style": {
|
||||
// https://drafts.csswg.org/css-ui/#propdef-outline-style
|
||||
tests: [
|
||||
discrete("none", "dotted")
|
||||
]
|
||||
},
|
||||
"overflow-clip-box": {
|
||||
// https://developer.mozilla.org/en/docs/Web/CSS/overflow-clip-box
|
||||
tests: [
|
||||
discrete("padding-box", "content-box")
|
||||
]
|
||||
},
|
||||
"overflow-wrap": {
|
||||
// https://drafts.csswg.org/css-text-3/#propdef-overflow-wrap
|
||||
tests: [
|
||||
discrete("normal", "break-word")
|
||||
]
|
||||
},
|
||||
"overflow-x": {
|
||||
// https://drafts.csswg.org/css-overflow-3/#propdef-overflow-x
|
||||
tests: [
|
||||
discrete("visible", "hidden")
|
||||
]
|
||||
},
|
||||
"overflow-y": {
|
||||
// https://drafts.csswg.org/css-overflow-3/#propdef-overflow-y
|
||||
tests: [
|
||||
discrete("visible", "hidden")
|
||||
]
|
||||
},
|
||||
"page-break-after": {
|
||||
// https://drafts.csswg.org/css-break-3/#propdef-break-after
|
||||
tests: [
|
||||
discrete("always", "auto")
|
||||
]
|
||||
},
|
||||
"page-break-before": {
|
||||
// https://drafts.csswg.org/css-break-3/#propdef-break-before
|
||||
tests: [
|
||||
discrete("always", "auto")
|
||||
]
|
||||
},
|
||||
"page-break-inside": {
|
||||
// https://drafts.csswg.org/css-break-3/#propdef-break-inside
|
||||
tests: [
|
||||
discrete("auto", "avoid")
|
||||
]
|
||||
},
|
||||
"paint-order": {
|
||||
// https://svgwg.org/svg2-draft/painting.html#PaintOrderProperty
|
||||
tests: [
|
||||
discrete("fill", "stroke")
|
||||
]
|
||||
},
|
||||
"pointer-events": {
|
||||
// https://svgwg.org/svg2-draft/interact.html#PointerEventsProperty
|
||||
tests: [
|
||||
discrete("fill", "none")
|
||||
]
|
||||
},
|
||||
"position": {
|
||||
// https://drafts.csswg.org/css-position/#propdef-position
|
||||
tests: [
|
||||
discrete("absolute", "fixed")
|
||||
]
|
||||
},
|
||||
"quotes": {
|
||||
// https://drafts.csswg.org/css-content-3/#propdef-quotes
|
||||
tests: [
|
||||
discrete("\"“\" \"”\" \"‘\" \"’\"", "\"‘\" \"’\" \"“\" \"”\"")
|
||||
]
|
||||
},
|
||||
"resize": {
|
||||
// https://drafts.csswg.org/css-ui/#propdef-resize
|
||||
tests: [
|
||||
discrete("both", "horizontal")
|
||||
]
|
||||
},
|
||||
"ruby-align": {
|
||||
// https://drafts.csswg.org/css-ruby-1/#propdef-ruby-align
|
||||
tests: [
|
||||
@ -156,6 +667,30 @@ var gCSSProperties = {
|
||||
],
|
||||
tagName: "ruby"
|
||||
},
|
||||
"scroll-behavior": {
|
||||
// https://drafts.csswg.org/cssom-view/#propdef-scroll-behavior
|
||||
tests: [
|
||||
discrete("auto", "smooth")
|
||||
]
|
||||
},
|
||||
"scroll-snap-type-x": {
|
||||
// https://developer.mozilla.org/en/docs/Web/CSS/scroll-snap-type-x
|
||||
tests: [
|
||||
discrete("mandatory", "proximity")
|
||||
]
|
||||
},
|
||||
"scroll-snap-type-y": {
|
||||
// https://developer.mozilla.org/en/docs/Web/CSS/scroll-snap-type-y
|
||||
tests: [
|
||||
discrete("mandatory", "proximity")
|
||||
]
|
||||
},
|
||||
"shape-outside": {
|
||||
// http://dev.w3.org/csswg/css-shapes/#propdef-shape-outside
|
||||
tests: [
|
||||
discrete("url(\"http://localhost/test-1\")", "url(\"http://localhost/test-2\")")
|
||||
]
|
||||
},
|
||||
"shape-rendering": {
|
||||
// https://svgwg.org/svg2-draft/painting.html#ShapeRenderingProperty
|
||||
tests: [
|
||||
@ -175,6 +710,24 @@ var gCSSProperties = {
|
||||
],
|
||||
tagName: "rect"
|
||||
},
|
||||
"table-layout": {
|
||||
// https://drafts.csswg.org/css-tables/#propdef-table-layout
|
||||
tests: [
|
||||
discrete("auto", "fixed")
|
||||
]
|
||||
},
|
||||
"text-align": {
|
||||
// https://drafts.csswg.org/css-text-3/#propdef-text-align
|
||||
tests: [
|
||||
discrete("start", "end")
|
||||
]
|
||||
},
|
||||
"text-align-last": {
|
||||
// https://drafts.csswg.org/css-text-3/#propdef-text-align-last
|
||||
tests: [
|
||||
discrete("start", "end")
|
||||
]
|
||||
},
|
||||
"text-anchor": {
|
||||
// https://svgwg.org/svg2-draft/text.html#TextAnchorProperty
|
||||
tests: [
|
||||
@ -184,7 +737,7 @@ var gCSSProperties = {
|
||||
"text-combine-upright": {
|
||||
// https://drafts.csswg.org/css-writing-modes-3/#propdef-text-combine-upright
|
||||
tests: [
|
||||
discrete("all", "digits")
|
||||
discrete("all", "none")
|
||||
]
|
||||
},
|
||||
"text-decoration-line": {
|
||||
@ -193,18 +746,72 @@ var gCSSProperties = {
|
||||
discrete("underline", "overline")
|
||||
]
|
||||
},
|
||||
"text-decoration-style": {
|
||||
// http://dev.w3.org/csswg/css-text-decor-3/#propdef-text-decoration-style
|
||||
tests: [
|
||||
discrete("solid", "dotted")
|
||||
]
|
||||
},
|
||||
"text-emphasis-position": {
|
||||
// http://dev.w3.org/csswg/css-text-decor-3/#propdef-text-emphasis-position
|
||||
tests: [
|
||||
discrete("over right", "under left")
|
||||
]
|
||||
},
|
||||
"text-emphasis-style": {
|
||||
// http://dev.w3.org/csswg/css-text-decor-3/#propdef-text-emphasis-style
|
||||
tests: [
|
||||
discrete("filled circle", "open dot")
|
||||
]
|
||||
},
|
||||
"text-orientation": {
|
||||
// https://drafts.csswg.org/css-writing-modes-3/#propdef-text-orientation
|
||||
tests: [
|
||||
discrete("upright", "sideways")
|
||||
]
|
||||
},
|
||||
"text-overflow": {
|
||||
// https://drafts.csswg.org/css-ui/#propdef-text-overflow
|
||||
tests: [
|
||||
discrete("clip", "ellipsis")
|
||||
]
|
||||
},
|
||||
"text-rendering": {
|
||||
// https://svgwg.org/svg2-draft/painting.html#TextRenderingProperty
|
||||
tests: [
|
||||
discrete("optimizeSpeed", "optimizeLegibility")
|
||||
]
|
||||
},
|
||||
"text-transform": {
|
||||
// https://drafts.csswg.org/css-text-3/#propdef-text-transform
|
||||
tests: [
|
||||
discrete("capitalize", "uppercase")
|
||||
]
|
||||
},
|
||||
"touch-action": {
|
||||
// https://w3c.github.io/pointerevents/#the-touch-action-css-property
|
||||
tests: [
|
||||
discrete("auto", "none")
|
||||
]
|
||||
},
|
||||
"transform-box": {
|
||||
// https://drafts.csswg.org/css-transforms/#propdef-transform-box
|
||||
tests: [
|
||||
discrete("fill-box", "border-box")
|
||||
]
|
||||
},
|
||||
"transform-style": {
|
||||
// https://drafts.csswg.org/css-transforms/#propdef-transform-style
|
||||
tests: [
|
||||
discrete("flat", "preserve-3d")
|
||||
]
|
||||
},
|
||||
"unicode-bidi": {
|
||||
// https://drafts.csswg.org/css-writing-modes-3/#propdef-unicode-bidi
|
||||
tests: [
|
||||
discrete("embed", "bidi-override")
|
||||
]
|
||||
},
|
||||
"vector-effect": {
|
||||
// https://svgwg.org/svg2-draft/coords.html#VectorEffectProperty
|
||||
tests: [
|
||||
@ -217,12 +824,24 @@ var gCSSProperties = {
|
||||
visibility()
|
||||
]
|
||||
},
|
||||
"white-space": {
|
||||
// https://drafts.csswg.org/css-text-4/#propdef-white-space
|
||||
tests: [
|
||||
discrete("pre", "nowrap")
|
||||
]
|
||||
},
|
||||
"word-break": {
|
||||
// https://drafts.csswg.org/css-text-3/#propdef-word-break
|
||||
tests: [
|
||||
discrete("keep-all", "break-all")
|
||||
]
|
||||
},
|
||||
"will-change": {
|
||||
// http://dev.w3.org/csswg/css-will-change/#propdef-will-change
|
||||
tests: [
|
||||
discrete("scroll-position", "contents")
|
||||
]
|
||||
},
|
||||
"writing-mode": {
|
||||
// https://drafts.csswg.org/css-writing-modes-3/#propdef-writing-mode
|
||||
tests: [
|
||||
@ -247,7 +866,7 @@ function discrete(from, to) {
|
||||
var idlName = propertyToIDL(property);
|
||||
var keyframes = {};
|
||||
keyframes[idlName] = [from, to];
|
||||
var target = createElement(t, options.tagName);
|
||||
var target = createTestElement(t, options.tagName);
|
||||
var animation = target.animate(keyframes,
|
||||
{ duration: 1000, fill: "both" });
|
||||
testAnimationSamples(animation, idlName,
|
||||
@ -265,7 +884,7 @@ function discrete(from, to) {
|
||||
var idlName = propertyToIDL(property);
|
||||
var keyframes = {};
|
||||
keyframes[idlName] = [from, to];
|
||||
var target = createElement(t, options.tagName);
|
||||
var target = createTestElement(t, options.tagName);
|
||||
var animation = target.animate(keyframes,
|
||||
{ duration: 1000, fill: "both",
|
||||
easing: "cubic-bezier(0.68,0,1,0.01)" });
|
||||
@ -284,7 +903,7 @@ function discrete(from, to) {
|
||||
var keyframes = {};
|
||||
keyframes[idlName] = [from, to];
|
||||
keyframes.easing = "cubic-bezier(0.68,0,1,0.01)";
|
||||
var target = createElement(t, options.tagName);
|
||||
var target = createTestElement(t, options.tagName);
|
||||
var animation = target.animate(keyframes,
|
||||
{ duration: 1000, fill: "both" });
|
||||
testAnimationSamples(animation, idlName,
|
||||
@ -302,7 +921,7 @@ function length() {
|
||||
var idlName = propertyToIDL(property);
|
||||
var keyframes = {};
|
||||
keyframes[idlName] = ["10px", "50px"];
|
||||
var target = createElement(t, options.tagName);
|
||||
var target = createTestElement(t, options.tagName);
|
||||
var animation = target.animate(keyframes,
|
||||
{ duration: 1000, fill: "both" });
|
||||
testAnimationSamples(animation, idlName,
|
||||
@ -315,7 +934,7 @@ function length() {
|
||||
var idlName = propertyToIDL(property);
|
||||
var keyframes = {};
|
||||
keyframes[idlName] = ["1rem", "5rem"];
|
||||
var target = createElement(t, options.tagName);
|
||||
var target = createTestElement(t, options.tagName);
|
||||
var animation = target.animate(keyframes,
|
||||
{ duration: 1000, fill: "both" });
|
||||
testAnimationSamples(animation, idlName,
|
||||
@ -332,7 +951,7 @@ function percentage() {
|
||||
var idlName = propertyToIDL(property);
|
||||
var keyframes = {};
|
||||
keyframes[idlName] = ["10%", "50%"];
|
||||
var target = createElement(t, options.tagName);
|
||||
var target = createTestElement(t, options.tagName);
|
||||
var animation = target.animate(keyframes,
|
||||
{ duration: 1000, fill: "both" });
|
||||
testAnimationSamples(animation, idlName,
|
||||
@ -349,7 +968,7 @@ function integer() {
|
||||
var idlName = propertyToIDL(property);
|
||||
var keyframes = {};
|
||||
keyframes[idlName] = [-2, 2];
|
||||
var target = createElement(t, options.tagName);
|
||||
var target = createTestElement(t, options.tagName);
|
||||
var animation = target.animate(keyframes,
|
||||
{ duration: 1000, fill: "both" });
|
||||
testAnimationSamples(animation, idlName,
|
||||
@ -366,7 +985,7 @@ function positiveNumber() {
|
||||
var idlName = propertyToIDL(property);
|
||||
var keyframes = {};
|
||||
keyframes[idlName] = [1.1, 1.5];
|
||||
var target = createElement(t, options.tagName);
|
||||
var target = createTestElement(t, options.tagName);
|
||||
var animation = target.animate(keyframes,
|
||||
{ duration: 1000, fill: "both" });
|
||||
testAnimationSamples(animation, idlName,
|
||||
@ -386,7 +1005,7 @@ function lengthPercentageOrCalc() {
|
||||
var idlName = propertyToIDL(property);
|
||||
var keyframes = {};
|
||||
keyframes[idlName] = ["10px", "20%"];
|
||||
var target = createElement(t, options.tagName);
|
||||
var target = createTestElement(t, options.tagName);
|
||||
var animation = target.animate(keyframes,
|
||||
{ duration: 1000, fill: "both" });
|
||||
testAnimationSamples(animation, idlName,
|
||||
@ -399,7 +1018,7 @@ function lengthPercentageOrCalc() {
|
||||
var idlName = propertyToIDL(property);
|
||||
var keyframes = {};
|
||||
keyframes[idlName] = ["10%", "2em"];
|
||||
var target = createElement(t, options.tagName);
|
||||
var target = createTestElement(t, options.tagName);
|
||||
var animation = target.animate(keyframes,
|
||||
{ duration: 1000, fill: "both" });
|
||||
testAnimationSamples(animation, idlName,
|
||||
@ -412,7 +1031,7 @@ function lengthPercentageOrCalc() {
|
||||
var idlName = propertyToIDL(property);
|
||||
var keyframes = {};
|
||||
keyframes[idlName] = ["1em", "2rem"];
|
||||
var target = createElement(t, options.tagName);
|
||||
var target = createTestElement(t, options.tagName);
|
||||
var animation = target.animate(keyframes,
|
||||
{ duration: 1000, fill: "both" });
|
||||
testAnimationSamples(animation, idlName,
|
||||
@ -425,7 +1044,7 @@ function lengthPercentageOrCalc() {
|
||||
var idlName = propertyToIDL(property);
|
||||
var keyframes = {};
|
||||
keyframes[idlName] = ["10px", "calc(1em + 20%)"];
|
||||
var target = createElement(t, options.tagName);
|
||||
var target = createTestElement(t, options.tagName);
|
||||
var animation = target.animate(keyframes,
|
||||
{ duration: 1000, fill: "both" });
|
||||
testAnimationSamples(animation, idlName,
|
||||
@ -438,7 +1057,7 @@ function lengthPercentageOrCalc() {
|
||||
var idlName = propertyToIDL(property);
|
||||
var keyframes = {};
|
||||
keyframes[idlName] = ["calc(10px + 10%)", "calc(1em + 1rem + 20%)"];
|
||||
var target = createElement(t, options.tagName);
|
||||
var target = createTestElement(t, options.tagName);
|
||||
var animation = target.animate(keyframes,
|
||||
{ duration: 1000, fill: "both" });
|
||||
testAnimationSamples(animation, idlName,
|
||||
@ -458,7 +1077,7 @@ function visibility() {
|
||||
var idlName = propertyToIDL(property);
|
||||
var keyframes = {};
|
||||
keyframes[idlName] = ["visible", "hidden"];
|
||||
var target = createElement(t, options.tagName);
|
||||
var target = createTestElement(t, options.tagName);
|
||||
var animation = target.animate(keyframes,
|
||||
{ duration: 1000, fill: "both" });
|
||||
testAnimationSamples(animation, idlName,
|
||||
@ -472,7 +1091,7 @@ function visibility() {
|
||||
var idlName = propertyToIDL(property);
|
||||
var keyframes = {};
|
||||
keyframes[idlName] = ["hidden", "visible"];
|
||||
var target = createElement(t, options.tagName);
|
||||
var target = createTestElement(t, options.tagName);
|
||||
var animation = target.animate(keyframes,
|
||||
{ duration: 1000, fill: "both" });
|
||||
testAnimationSamples(animation, idlName,
|
||||
@ -486,7 +1105,7 @@ function visibility() {
|
||||
var idlName = propertyToIDL(property);
|
||||
var keyframes = {};
|
||||
keyframes[idlName] = ["hidden", "collapse"];
|
||||
var target = createElement(t, options.tagName);
|
||||
var target = createTestElement(t, options.tagName);
|
||||
var animation = target.animate(keyframes,
|
||||
{ duration: 1000, fill: "both" });
|
||||
testAnimationSamples(animation, idlName,
|
||||
@ -504,7 +1123,7 @@ function visibility() {
|
||||
var idlName = propertyToIDL(property);
|
||||
var keyframes = {};
|
||||
keyframes[idlName] = ["visible", "hidden"];
|
||||
var target = createElement(t, options.tagName);
|
||||
var target = createTestElement(t, options.tagName);
|
||||
var animation =
|
||||
target.animate(keyframes,
|
||||
{ duration: 1000, fill: "both",
|
||||
@ -523,15 +1142,25 @@ function visibility() {
|
||||
}
|
||||
|
||||
function testAnimationSamples(animation, idlName, testSamples) {
|
||||
var target = animation.effect.target;
|
||||
var type = animation.effect.target.type;
|
||||
var target = type
|
||||
? animation.effect.target.parentElement
|
||||
: animation.effect.target;
|
||||
testSamples.forEach(function(testSample) {
|
||||
animation.currentTime = testSample.time;
|
||||
assert_equals(getComputedStyle(target)[idlName], testSample.expected,
|
||||
assert_equals(getComputedStyle(target, type)[idlName],
|
||||
testSample.expected,
|
||||
"The value should be " + testSample.expected +
|
||||
" at " + testSample.time + "ms");
|
||||
});
|
||||
}
|
||||
|
||||
function createTestElement(t, tagName) {
|
||||
return tagName && tagName.startsWith("::")
|
||||
? createPseudo(t, tagName.substring(2))
|
||||
: createElement(t, tagName);
|
||||
}
|
||||
|
||||
function isSupported(property) {
|
||||
var testKeyframe = new TestKeyframe(propertyToIDL(property));
|
||||
try {
|
||||
|
292
toolkit/components/places/ExtensionSearchHandler.jsm
Normal file
292
toolkit/components/places/ExtensionSearchHandler.jsm
Normal file
@ -0,0 +1,292 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = [ "ExtensionSearchHandler" ];
|
||||
|
||||
// Used to keep track of all of the registered keywords, where each keyword is
|
||||
// mapped to a KeywordInfo instance.
|
||||
let gKeywordMap = new Map();
|
||||
|
||||
// Used to keep track of the active input session.
|
||||
let gActiveInputSession = null;
|
||||
|
||||
// Used to keep track of who has control over the active suggestion callback
|
||||
// so older callbacks can be ignored. The callback ID should increment whenever
|
||||
// the input changes or the input session ends.
|
||||
let gCurrentCallbackID = 0;
|
||||
|
||||
// Handles keeping track of information associated to the registered keyword.
|
||||
class KeywordInfo {
|
||||
constructor(extension, description) {
|
||||
this._extension = extension;
|
||||
this._description = description;
|
||||
}
|
||||
|
||||
get description() {
|
||||
return this._description;
|
||||
}
|
||||
|
||||
set description(desc) {
|
||||
this._description = desc;
|
||||
}
|
||||
|
||||
get extension() {
|
||||
return this._extension;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsible for handling communication between the extension and the urlbar.
|
||||
class InputSession {
|
||||
constructor(keyword, extension) {
|
||||
this._keyword = keyword;
|
||||
this._extension = extension;
|
||||
this._suggestionsCallback = null;
|
||||
this._searchFinishedCallback = null;
|
||||
}
|
||||
|
||||
get keyword() {
|
||||
return this._keyword;
|
||||
}
|
||||
|
||||
addSuggestions(suggestions) {
|
||||
this._suggestionsCallback(suggestions);
|
||||
}
|
||||
|
||||
start(eventName) {
|
||||
this._extension.emit(eventName);
|
||||
}
|
||||
|
||||
update(eventName, text, suggestionsCallback, searchFinishedCallback) {
|
||||
if (this._searchFinishedCallback) {
|
||||
this._searchFinishedCallback();
|
||||
}
|
||||
this._searchFinishedCallback = searchFinishedCallback;
|
||||
this._suggestionsCallback = suggestionsCallback;
|
||||
this._extension.emit(eventName, text, ++gCurrentCallbackID);
|
||||
}
|
||||
|
||||
cancel(eventName) {
|
||||
this._searchFinishedCallback();
|
||||
this._extension.emit(eventName);
|
||||
}
|
||||
|
||||
end(eventName, text, disposition) {
|
||||
this._searchFinishedCallback();
|
||||
this._extension.emit(eventName, text, disposition);
|
||||
}
|
||||
}
|
||||
|
||||
var ExtensionSearchHandler = Object.freeze({
|
||||
MSG_INPUT_STARTED: "webext-omnibox-input-started",
|
||||
MSG_INPUT_CHANGED: "webext-omnibox-input-changed",
|
||||
MSG_INPUT_ENTERED: "webext-omnibox-input-entered",
|
||||
MSG_INPUT_CANCELLED: "webext-omnibox-input-cancelled",
|
||||
|
||||
/**
|
||||
* Registers a keyword.
|
||||
*
|
||||
* @param {string} keyword The keyword to register.
|
||||
* @param {Extension} extension The extension registering the keyword.
|
||||
*/
|
||||
registerKeyword(keyword, extension) {
|
||||
if (gKeywordMap.has(keyword)) {
|
||||
throw new Error(`The keyword provided is already registered: "${keyword}"`);
|
||||
}
|
||||
gKeywordMap.set(keyword, new KeywordInfo(extension, extension.name));
|
||||
},
|
||||
|
||||
/**
|
||||
* Unregisters a keyword.
|
||||
*
|
||||
* @param {string} keyword The keyword to unregister.
|
||||
*/
|
||||
unregisterKeyword(keyword) {
|
||||
if (!gKeywordMap.has(keyword)) {
|
||||
throw new Error(`The keyword provided is not registered: "${keyword}"`);
|
||||
}
|
||||
gActiveInputSession = null;
|
||||
gKeywordMap.delete(keyword);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if a keyword is registered.
|
||||
*
|
||||
* @param {string} keyword The word to check.
|
||||
* @return {boolean} true if the word is a registered keyword.
|
||||
*/
|
||||
isKeywordRegistered(keyword) {
|
||||
return gKeywordMap.has(keyword);
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {boolean} true if there is an active input session.
|
||||
*/
|
||||
hasActiveInputSession() {
|
||||
return gActiveInputSession != null;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} keyword The keyword to look up.
|
||||
* @return {string} the description to use for the heuristic result.
|
||||
*/
|
||||
getDescription(keyword) {
|
||||
if (!gKeywordMap.has(keyword)) {
|
||||
throw new Error(`The keyword provided is not registered: "${keyword}"`);
|
||||
}
|
||||
return gKeywordMap.get(keyword).description;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the default suggestion for the registered keyword. The suggestion's
|
||||
* description will be used for the comment in the heuristic result.
|
||||
*
|
||||
* @param {string} keyword The keyword.
|
||||
* @param {string} description The description to use for the heuristic result.
|
||||
*/
|
||||
setDefaultSuggestion(keyword, {description}) {
|
||||
if (!gKeywordMap.has(keyword)) {
|
||||
throw new Error(`The keyword provided is not registered: "${keyword}"`);
|
||||
}
|
||||
gKeywordMap.get(keyword).description = description;
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds suggestions for the registered keyword. This function will throw if
|
||||
* the keyword provided is not registered or active, or if the callback ID
|
||||
* provided is no longer equal to the active callback ID.
|
||||
*
|
||||
* @param {string} keyword The keyword.
|
||||
* @param {integer} id The ID of the suggestion callback.
|
||||
* @param {Array<Object>} suggestions An array of suggestions to provide to the urlbar.
|
||||
*/
|
||||
addSuggestions(keyword, id, suggestions) {
|
||||
if (!gKeywordMap.has(keyword)) {
|
||||
throw new Error(`The keyword provided is not registered: "${keyword}"`);
|
||||
}
|
||||
|
||||
if (!gActiveInputSession || gActiveInputSession.keyword != keyword) {
|
||||
throw new Error(`The keyword provided is not apart of an active input session: "${keyword}"`);
|
||||
}
|
||||
|
||||
if (id != gCurrentCallbackID) {
|
||||
throw new Error(`The callback is no longer active for the keyword provided: "${keyword}"`);
|
||||
}
|
||||
|
||||
gActiveInputSession.addSuggestions(suggestions);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the input in the urlbar begins with `<keyword><space>`.
|
||||
*
|
||||
* If the keyword is inactive, MSG_INPUT_STARTED is emitted and the
|
||||
* keyword is marked as active. If the keyword is followed by any text,
|
||||
* MSG_INPUT_CHANGED is fired with the current callback ID that can be
|
||||
* used to provide suggestions to the urlbar while the callback ID is active.
|
||||
* The callback is invalidated when either the input changes or the urlbar blurs.
|
||||
*
|
||||
* @param {string} keyword The keyword to handle.
|
||||
* @param {string} text The search text in the urlbar.
|
||||
* @param {Function} callback The callback used to provide search suggestions.
|
||||
* @return {Promise} promise that resolves when the current search is complete.
|
||||
*/
|
||||
handleSearch(keyword, text, callback) {
|
||||
if (!gKeywordMap.has(keyword)) {
|
||||
throw new Error(`The keyword provided is not registered: "${keyword}"`);
|
||||
}
|
||||
|
||||
if (gActiveInputSession && gActiveInputSession.keyword != keyword) {
|
||||
throw new Error("A different input session is already ongoing");
|
||||
}
|
||||
|
||||
if (!text || !text.startsWith(`${keyword} `)) {
|
||||
throw new Error(`The text provided must start with: "${keyword} "`);
|
||||
}
|
||||
|
||||
if (!callback) {
|
||||
throw new Error("A callback must be provided");
|
||||
}
|
||||
|
||||
// The search text in the urlbar currently starts with <keyword><space>, and
|
||||
// we only want the text that follows.
|
||||
text = text.substring(keyword.length + 1);
|
||||
|
||||
// We fire MSG_INPUT_STARTED once we have <keyword><space>, and only fire
|
||||
// MSG_INPUT_CHANGED when we have text to process. This is different from Chrome's
|
||||
// behavior, which always fires MSG_INPUT_STARTED right before MSG_INPUT_CHANGED
|
||||
// first fires, but this is a bug in Chrome according to https://crbug.com/258911.
|
||||
if (!gActiveInputSession) {
|
||||
gActiveInputSession = new InputSession(keyword, gKeywordMap.get(keyword).extension);
|
||||
gActiveInputSession.start(this.MSG_INPUT_STARTED);
|
||||
|
||||
// Resolve early if there is no text to process. There can be text to process when
|
||||
// the input starts if the user copy/pastes the text into the urlbar.
|
||||
if (!text.length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
gActiveInputSession.update(this.MSG_INPUT_CHANGED, text, callback, resolve);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the user clicks on a suggestion that was added by
|
||||
* an extension. MSG_INPUT_ENTERED is emitted to the extension with
|
||||
* the keyword, the current search string, and info about how the
|
||||
* the search should be handled. This ends the active input session.
|
||||
*
|
||||
* @param {string} keyword The keyword associated to the suggestion.
|
||||
* @param {string} text The search text in the urlbar.
|
||||
* @param {string} where How the page should be opened. Accepted values are:
|
||||
* "current": open the page in the same tab.
|
||||
* "tab": open the page in a new foreground tab.
|
||||
* "tabshifted": open the page in a new background tab.
|
||||
*/
|
||||
handleInputEntered(keyword, text, where) {
|
||||
if (!gKeywordMap.has(keyword)) {
|
||||
throw new Error(`The keyword provided is not registered: "${keyword}"`);
|
||||
}
|
||||
|
||||
if (gActiveInputSession && gActiveInputSession.keyword != keyword) {
|
||||
throw new Error("A different input session is already ongoing");
|
||||
}
|
||||
|
||||
if (!text || !text.startsWith(`${keyword} `)) {
|
||||
throw new Error(`The text provided must start with: "${keyword} "`);
|
||||
}
|
||||
|
||||
let dispositionMap = {
|
||||
current: "currentTab",
|
||||
tab: "newForegroundTab",
|
||||
tabshifted: "newBackgroundTab",
|
||||
}
|
||||
let disposition = dispositionMap[where];
|
||||
|
||||
if (!disposition) {
|
||||
throw new Error(`Invalid "where" argument: ${where}`);
|
||||
}
|
||||
|
||||
// The search text in the urlbar currently starts with <keyword><space>, and
|
||||
// we only want to send the text that follows.
|
||||
text = text.substring(keyword.length + 1);
|
||||
|
||||
gActiveInputSession.end(this.MSG_INPUT_ENTERED, text, disposition)
|
||||
gActiveInputSession = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* If the user has ended the keyword input session without accepting the input,
|
||||
* MSG_INPUT_CANCELLED is emitted and the input session is ended.
|
||||
*/
|
||||
handleInputCancelled() {
|
||||
if (!gActiveInputSession) {
|
||||
throw new Error("There is no active input session");
|
||||
}
|
||||
gActiveInputSession.cancel(this.MSG_INPUT_CANCELLED);
|
||||
gActiveInputSession = null;
|
||||
}
|
||||
});
|
@ -69,6 +69,11 @@ const FRECENCY_DEFAULT = 1000;
|
||||
// always try to have at least MINIMUM_LOCAL_MATCHES local matches.
|
||||
const MINIMUM_LOCAL_MATCHES = 6;
|
||||
|
||||
// Extensions are allowed to add suggestions if they have registered a keyword
|
||||
// with the omnibox API. This is the maximum number of suggestions an extension
|
||||
// is allowed to add for a given search string.
|
||||
const MAXIMUM_ALLOWED_EXTENSION_MATCHES = 6;
|
||||
|
||||
// A regex that matches "single word" hostnames for whitelisting purposes.
|
||||
// The hostname will already have been checked for general validity, so we
|
||||
// don't need to be exhaustive here, so allow dashes anywhere.
|
||||
@ -269,6 +274,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
||||
"resource://gre/modules/osfile.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
|
||||
"resource://gre/modules/PromiseUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionSearchHandler",
|
||||
"resource://gre/modules/ExtensionSearchHandler.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||
"resource://gre/modules/Task.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PlacesSearchAutocompleteProvider",
|
||||
@ -743,11 +750,16 @@ function Search(searchString, searchParam, autocompleteListener,
|
||||
|
||||
// The index to insert remote matches at.
|
||||
this._remoteMatchesStartIndex = 0;
|
||||
// The index to insert local matches at.
|
||||
|
||||
this._localMatchesStartIndex = 0;
|
||||
|
||||
// Counts the number of inserted local matches.
|
||||
this._localMatchesCount = 0;
|
||||
// Counts the number of inserted remote matches.
|
||||
this._remoteMatchesCount = 0;
|
||||
// Counts the number of inserted extension matches.
|
||||
this._extensionMatchesCount = 0;
|
||||
}
|
||||
|
||||
Search.prototype = {
|
||||
@ -970,7 +982,19 @@ Search.prototype = {
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure to fill any remaining space.
|
||||
// Only add extension suggestions if the first token is a registered keyword
|
||||
// and the search string has characters after the first token.
|
||||
if (ExtensionSearchHandler.isKeywordRegistered(this._searchTokens[0]) &&
|
||||
this._originalSearchString.length > this._searchTokens[0].length) {
|
||||
yield this._matchExtensionSuggestions();
|
||||
if (!this.pending)
|
||||
return;
|
||||
} else if (ExtensionSearchHandler.hasActiveInputSession()) {
|
||||
ExtensionSearchHandler.handleInputCancelled();
|
||||
}
|
||||
|
||||
// Ensure to fill any remaining space. Suggestions which come from extensions are
|
||||
// inserted at the beginning, so any suggestions
|
||||
yield Promise.all(this._remoteMatchesPromises);
|
||||
}),
|
||||
|
||||
@ -978,7 +1002,15 @@ Search.prototype = {
|
||||
// We always try to make the first result a special "heuristic" result. The
|
||||
// heuristics below determine what type of result it will be, if any.
|
||||
|
||||
let hasSearchTerms = this._searchTokens.length > 0 ;
|
||||
let hasSearchTerms = this._searchTokens.length > 0;
|
||||
|
||||
if (hasSearchTerms) {
|
||||
// It may be a keyword registered by an extension.
|
||||
let matched = yield this._matchExtensionHeuristicResult();
|
||||
if (matched) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._enableActions && hasSearchTerms) {
|
||||
// It may be a search engine with an alias - which works like a keyword.
|
||||
@ -1152,6 +1184,16 @@ Search.prototype = {
|
||||
return gotResult;
|
||||
},
|
||||
|
||||
_matchExtensionHeuristicResult: function* () {
|
||||
if (ExtensionSearchHandler.isKeywordRegistered(this._searchTokens[0]) &&
|
||||
this._originalSearchString.length > this._searchTokens[0].length) {
|
||||
let description = ExtensionSearchHandler.getDescription(this._searchTokens[0]);
|
||||
this._addExtensionMatch(this._originalSearchString, description);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
_matchPlacesKeyword: function* () {
|
||||
// The first word could be a keyword, so that's what we'll search.
|
||||
let keyword = this._searchTokens[0];
|
||||
@ -1264,6 +1306,24 @@ Search.prototype = {
|
||||
return true;
|
||||
},
|
||||
|
||||
_addExtensionMatch(content, comment) {
|
||||
if (this._extensionMatchesCount >= MAXIMUM_ALLOWED_EXTENSION_MATCHES) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._addMatch({
|
||||
value: PlacesUtils.mozActionURI("extension", {
|
||||
content,
|
||||
keyword: this._searchTokens[0]
|
||||
}),
|
||||
comment,
|
||||
icon: "chrome://browser/content/extension.svg",
|
||||
style: "action extension",
|
||||
frecency: FRECENCY_DEFAULT,
|
||||
extension: true,
|
||||
});
|
||||
},
|
||||
|
||||
_addSearchEngineMatch(match, query, suggestion) {
|
||||
let actionURLParams = {
|
||||
engineName: match.engineName,
|
||||
@ -1287,6 +1347,18 @@ Search.prototype = {
|
||||
});
|
||||
},
|
||||
|
||||
*_matchExtensionSuggestions() {
|
||||
let promise = ExtensionSearchHandler.handleSearch(this._searchTokens[0], this._originalSearchString,
|
||||
suggestions => {
|
||||
suggestions.forEach(suggestion => {
|
||||
let content = `${this._searchTokens[0]} ${suggestion.content}`;
|
||||
this._addExtensionMatch(content, suggestion.description);
|
||||
});
|
||||
}
|
||||
);
|
||||
this._remoteMatchesPromises.push(promise);
|
||||
},
|
||||
|
||||
*_matchRemoteTabs() {
|
||||
let matches = yield PlacesRemoteTabsAutocompleteProvider.getMatches(this._originalSearchString);
|
||||
for (let {url, title, icon, deviceName} of matches) {
|
||||
@ -1492,6 +1564,11 @@ Search.prototype = {
|
||||
// Append after local matches.
|
||||
index = this._remoteMatchesStartIndex + this._remoteMatchesCount;
|
||||
this._remoteMatchesCount++;
|
||||
} else if (match.extension) {
|
||||
index = this._localMatchesStartIndex;
|
||||
this._localMatchesStartIndex++;
|
||||
this._remoteMatchesStartIndex++;
|
||||
this._extensionMatchesCount++;
|
||||
} else {
|
||||
// This is a local match.
|
||||
if (match.frecency > FRECENCY_DEFAULT ||
|
||||
|
@ -64,6 +64,7 @@ if CONFIG['MOZ_PLACES']:
|
||||
'ClusterLib.js',
|
||||
'ColorAnalyzer_worker.js',
|
||||
'ColorConversion.js',
|
||||
'ExtensionSearchHandler.jsm',
|
||||
'History.jsm',
|
||||
'PlacesBackups.jsm',
|
||||
'PlacesDBUtils.jsm',
|
||||
|
@ -167,20 +167,21 @@ function* check_autocomplete(test) {
|
||||
do_print("onSearchBegin received");
|
||||
numSearchesStarted++;
|
||||
};
|
||||
let deferred = Promise.defer();
|
||||
input.onSearchComplete = () => {
|
||||
do_print("onSearchComplete received");
|
||||
deferred.resolve();
|
||||
}
|
||||
|
||||
let searchCompletePromise = new Promise(resolve => {
|
||||
input.onSearchComplete = () => {
|
||||
do_print("onSearchComplete received");
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
let expectedSearches = 1;
|
||||
if (test.incompleteSearch) {
|
||||
controller.startSearch(test.incompleteSearch);
|
||||
expectedSearches++;
|
||||
}
|
||||
|
||||
do_print("Searching for: '" + test.search + "'");
|
||||
controller.startSearch(test.search);
|
||||
yield deferred.promise;
|
||||
yield searchCompletePromise;
|
||||
|
||||
Assert.equal(numSearchesStarted, expectedSearches, "All searches started");
|
||||
|
||||
@ -415,6 +416,22 @@ function makeSwitchToTabMatch(url, extra = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function makeExtensionMatch(extra = {}) {
|
||||
let style = [ "action", "extension" ];
|
||||
if (extra.heuristic) {
|
||||
style.push("heuristic");
|
||||
}
|
||||
|
||||
return {
|
||||
uri: makeActionURI("extension", {
|
||||
content: extra.content,
|
||||
keyword: extra.keyword,
|
||||
}),
|
||||
title: extra.description,
|
||||
style,
|
||||
};
|
||||
}
|
||||
|
||||
function setFaviconForHref(href, iconHref) {
|
||||
return new Promise(resolve => {
|
||||
PlacesUtils.favicons.setAndFetchFaviconForPage(
|
||||
|
@ -0,0 +1,384 @@
|
||||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
|
||||
* vim:set ts=2 sw=2 sts=2 et:
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
Cu.import("resource://gre/modules/ExtensionSearchHandler.jsm");
|
||||
|
||||
let controller = Cc["@mozilla.org/autocomplete/controller;1"].getService(Ci.nsIAutoCompleteController);
|
||||
|
||||
add_task(function* test_correct_errors_are_thrown() {
|
||||
let keyword = "foo";
|
||||
let anotherKeyword = "bar";
|
||||
let unregisteredKeyword = "baz";
|
||||
|
||||
// Register a keyword.
|
||||
ExtensionSearchHandler.registerKeyword(keyword, { emit: () => {} });
|
||||
|
||||
// Try registering the keyword again.
|
||||
Assert.throws(() => ExtensionSearchHandler.registerKeyword(keyword, { emit: () => {} }));
|
||||
|
||||
// Register a different keyword.
|
||||
ExtensionSearchHandler.registerKeyword(anotherKeyword, { emit: () => {} });
|
||||
|
||||
// Try calling handleSearch for an unregistered keyword.
|
||||
Assert.throws(() => ExtensionSearchHandler.handleSearch(unregisteredKeyword, `${unregisteredKeyword} `, () => {}));
|
||||
|
||||
// Try calling handleSearch without a callback.
|
||||
Assert.throws(() => ExtensionSearchHandler.handleSearch(unregisteredKeyword, `${unregisteredKeyword} `));
|
||||
|
||||
// Try getting the description for a keyword which isn't registered.
|
||||
Assert.throws(() => ExtensionSearchHandler.getDescription(unregisteredKeyword));
|
||||
|
||||
// Try getting the extension name for a keyword which isn't registered.
|
||||
Assert.throws(() => ExtensionSearchHandler.getExtensionName(unregisteredKeyword));
|
||||
|
||||
// Try setting the default suggestion for a keyword which isn't registered.
|
||||
Assert.throws(() => ExtensionSearchHandler.setDefaultSuggestion(unregisteredKeyword, "suggestion"));
|
||||
|
||||
// Try calling handleInputCancelled when there is no active input session.
|
||||
Assert.throws(() => ExtensionSearchHandler.handleInputCancelled());
|
||||
|
||||
// Try calling handleInputEntered when there is no active input session.
|
||||
Assert.throws(() => ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} test`, "tab"));
|
||||
|
||||
// Start a session by calling handleSearch with the registered keyword.
|
||||
ExtensionSearchHandler.handleSearch(keyword, `${keyword} test`, () => {});
|
||||
|
||||
// Try providing suggestions for an unregistered keyword.
|
||||
Assert.throws(() => ExtensionSearchHandler.addSuggestions(unregisteredKeyword, 0, []));
|
||||
|
||||
// Try providing suggestions for an inactive keyword.
|
||||
Assert.throws(() => ExtensionSearchHandler.addSuggestions(anotherKeyword, 0, []));
|
||||
|
||||
// Try calling handleSearch for an inactive keyword.
|
||||
Assert.throws(() => ExtensionSearchHandler.handleSearch(anotherKeyword, `${anotherKeyword} `, () => {}));
|
||||
|
||||
// Try calling addSuggestions with an old callback ID.
|
||||
Assert.throws(() => ExtensionSearchHandler.addSuggestions(keyword, 0, []));
|
||||
|
||||
// Add suggestions with a valid callback ID.
|
||||
ExtensionSearchHandler.addSuggestions(keyword, 1, []);
|
||||
|
||||
// Add suggestions again with a valid callback ID.
|
||||
ExtensionSearchHandler.addSuggestions(keyword, 1, []);
|
||||
|
||||
// Try calling addSuggestions with a future callback ID.
|
||||
Assert.throws(() => ExtensionSearchHandler.addSuggestions(keyword, 2, []));
|
||||
|
||||
// End the input session by calling handleInputCancelled.
|
||||
ExtensionSearchHandler.handleInputCancelled();
|
||||
|
||||
// Try calling handleInputCancelled after the session has ended.
|
||||
Assert.throws(() => ExtensionSearchHandler.handleInputCancelled());
|
||||
|
||||
// Try calling handleSearch that doesn't have a space after the keyword.
|
||||
Assert.throws(() => ExtensionSearchHandler.handleSearch(anotherKeyword, `${anotherKeyword}`, () => {}));
|
||||
|
||||
// Try calling handleSearch with text starting with the wrong keyword.
|
||||
Assert.throws(() => ExtensionSearchHandler.handleSearch(anotherKeyword, `${keyword} test`, () => {}));
|
||||
|
||||
// Start a new session by calling handleSearch with a different keyword
|
||||
ExtensionSearchHandler.handleSearch(anotherKeyword, `${anotherKeyword} test`, () => {});
|
||||
|
||||
// Try adding suggestions again with the same callback ID now that the input session has ended.
|
||||
Assert.throws(() => ExtensionSearchHandler.addSuggestions(keyword, 1, []));
|
||||
|
||||
// Add suggestions with a valid callback ID.
|
||||
ExtensionSearchHandler.addSuggestions(anotherKeyword, 2, []);
|
||||
|
||||
// Try adding suggestions with a valid callback ID but a different keyword.
|
||||
Assert.throws(() => ExtensionSearchHandler.addSuggestions(keyword, 2, []));
|
||||
|
||||
// Try adding suggestions with a valid callback ID but an unregistered keyword.
|
||||
Assert.throws(() => ExtensionSearchHandler.addSuggestions(unregisteredKeyword, 2, []));
|
||||
|
||||
// Set the default suggestion.
|
||||
ExtensionSearchHandler.setDefaultSuggestion(anotherKeyword, {description: "test result"});
|
||||
|
||||
// Try ending the session using handleInputEntered with a different keyword.
|
||||
Assert.throws(() => ExtensionSearchHandler.handleInputEntered(keyword, `${keyword} test`, "tab"));
|
||||
|
||||
// Try calling handleInputEntered with invalid text.
|
||||
Assert.throws(() => ExtensionSearchHandler.handleInputEntered(anotherKeyword, ` test`, "tab"));
|
||||
|
||||
// Try calling handleInputEntered with an invalid disposition.
|
||||
Assert.throws(() => ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} test`, "invalid"));
|
||||
|
||||
// End the session by calling handleInputEntered.
|
||||
ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} test`, "tab");
|
||||
|
||||
// Try calling handleInputEntered after the session has ended.
|
||||
Assert.throws(() => ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} test`, "tab"));
|
||||
|
||||
// Unregister the keyword.
|
||||
ExtensionSearchHandler.unregisterKeyword(keyword);
|
||||
|
||||
// Try setting the default suggestion for the unregistered keyword.
|
||||
Assert.throws(() => ExtensionSearchHandler.setDefaultSuggestion(keyword, {description: "test"}));
|
||||
|
||||
// Try handling a search with the unregistered keyword.
|
||||
Assert.throws(() => ExtensionSearchHandler.handleSearch(keyword, `${keyword} test`, () => {}));
|
||||
|
||||
// Try unregistering the keyword again.
|
||||
Assert.throws(() => ExtensionSearchHandler.unregisterKeyword(keyword));
|
||||
|
||||
// Unregister the other keyword.
|
||||
ExtensionSearchHandler.unregisterKeyword(anotherKeyword);
|
||||
|
||||
// Try unregistering the word which was never registered.
|
||||
Assert.throws(() => ExtensionSearchHandler.unregisterKeyword(unregisteredKeyword));
|
||||
|
||||
// Try setting the default suggestion for a word that was never registered.
|
||||
Assert.throws(() => ExtensionSearchHandler.setDefaultSuggestion(unregisteredKeyword, {description: "test"}));
|
||||
|
||||
yield cleanup();
|
||||
});
|
||||
|
||||
add_task(function* test_correct_events_are_emitted() {
|
||||
let events = [];
|
||||
function checkEvents(expectedEvents) {
|
||||
Assert.equal(events.length, expectedEvents.length, "The correct number of events fired");
|
||||
expectedEvents.forEach((e, i) => Assert.equal(e, events[i], `Expected "${e}" event to fire`));
|
||||
events = [];
|
||||
}
|
||||
|
||||
let mockExtension = { emit: message => events.push(message) };
|
||||
|
||||
let keyword = "foo";
|
||||
let anotherKeyword = "bar";
|
||||
|
||||
ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
|
||||
ExtensionSearchHandler.registerKeyword(anotherKeyword, mockExtension);
|
||||
|
||||
ExtensionSearchHandler.handleSearch(keyword, `${keyword} `, () => {});
|
||||
checkEvents([ExtensionSearchHandler.MSG_INPUT_STARTED]);
|
||||
|
||||
ExtensionSearchHandler.handleSearch(keyword, `${keyword} f`, () => {});
|
||||
checkEvents([ExtensionSearchHandler.MSG_INPUT_CHANGED]);
|
||||
|
||||
ExtensionSearchHandler.handleInputEntered(keyword, `${keyword} f`, "tab");
|
||||
checkEvents([ExtensionSearchHandler.MSG_INPUT_ENTERED]);
|
||||
|
||||
ExtensionSearchHandler.handleSearch(keyword, `${keyword} f`, () => {});
|
||||
checkEvents([
|
||||
ExtensionSearchHandler.MSG_INPUT_STARTED,
|
||||
ExtensionSearchHandler.MSG_INPUT_CHANGED
|
||||
]);
|
||||
|
||||
ExtensionSearchHandler.handleInputCancelled();
|
||||
checkEvents([ExtensionSearchHandler.MSG_INPUT_CANCELLED]);
|
||||
|
||||
ExtensionSearchHandler.handleSearch(anotherKeyword, `${anotherKeyword} baz`, () => {});
|
||||
checkEvents([
|
||||
ExtensionSearchHandler.MSG_INPUT_STARTED,
|
||||
ExtensionSearchHandler.MSG_INPUT_CHANGED
|
||||
]);
|
||||
|
||||
ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} baz`, "tab");
|
||||
checkEvents([ExtensionSearchHandler.MSG_INPUT_ENTERED]);
|
||||
|
||||
ExtensionSearchHandler.unregisterKeyword(keyword);
|
||||
});
|
||||
|
||||
add_task(function* test_removes_suggestion_if_its_content_is_typed_in() {
|
||||
let keyword = "test";
|
||||
let extensionName = "Foo Bar";
|
||||
|
||||
let mockExtension = {
|
||||
name: extensionName,
|
||||
emit(message, text, id) {
|
||||
if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
|
||||
ExtensionSearchHandler.addSuggestions(keyword, id, [
|
||||
{content: "foo", description: "first suggestion"},
|
||||
{content: "bar", description: "second suggestion"},
|
||||
{content: "baz", description: "third suggestion"},
|
||||
]);
|
||||
controller.stopSearch();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
|
||||
|
||||
yield check_autocomplete({
|
||||
search: `${keyword} unmatched`,
|
||||
searchParam: "enable-actions",
|
||||
matches: [
|
||||
makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} unmatched`}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} foo`, description: "first suggestion"}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} bar`, description: "second suggestion"}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} baz`, description: "third suggestion"})
|
||||
]
|
||||
});
|
||||
|
||||
yield check_autocomplete({
|
||||
search: `${keyword} foo`,
|
||||
searchParam: "enable-actions",
|
||||
matches: [
|
||||
makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} foo`}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} bar`, description: "second suggestion"}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} baz`, description: "third suggestion"})
|
||||
]
|
||||
});
|
||||
|
||||
yield check_autocomplete({
|
||||
search: `${keyword} bar`,
|
||||
searchParam: "enable-actions",
|
||||
matches: [
|
||||
makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} bar`}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} foo`, description: "first suggestion"}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} baz`, description: "third suggestion"})
|
||||
]
|
||||
});
|
||||
|
||||
yield check_autocomplete({
|
||||
search: `${keyword} baz`,
|
||||
searchParam: "enable-actions",
|
||||
matches: [
|
||||
makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} baz`}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} foo`, description: "first suggestion"}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} bar`, description: "second suggestion"})
|
||||
]
|
||||
});
|
||||
|
||||
ExtensionSearchHandler.unregisterKeyword(keyword);
|
||||
yield cleanup();
|
||||
});
|
||||
|
||||
add_task(function* test_extension_results_should_come_first() {
|
||||
let keyword = "test";
|
||||
let extensionName = "Omnibox Example";
|
||||
|
||||
let uri = NetUtil.newURI(`http://a.com/b`);
|
||||
yield PlacesTestUtils.addVisits([
|
||||
{ uri, title: `${keyword} -` },
|
||||
]);
|
||||
|
||||
let mockExtension = {
|
||||
name: extensionName,
|
||||
emit(message, text, id) {
|
||||
if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
|
||||
ExtensionSearchHandler.addSuggestions(keyword, id, [
|
||||
{content: "foo", description: "first suggestion"},
|
||||
{content: "bar", description: "second suggestion"},
|
||||
{content: "baz", description: "third suggestion"},
|
||||
]);
|
||||
}
|
||||
controller.stopSearch();
|
||||
}
|
||||
};
|
||||
|
||||
ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
|
||||
|
||||
// Start an input session before testing MSG_INPUT_CHANGED.
|
||||
ExtensionSearchHandler.handleSearch(keyword, `${keyword} `, () => {});
|
||||
|
||||
yield check_autocomplete({
|
||||
search: `${keyword} -`,
|
||||
searchParam: "enable-actions",
|
||||
matches: [
|
||||
makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} -`}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} foo`, description: "first suggestion"}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} bar`, description: "second suggestion"}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} baz`, description: "third suggestion"}),
|
||||
{ uri, title: `${keyword} -` }
|
||||
]
|
||||
});
|
||||
|
||||
ExtensionSearchHandler.unregisterKeyword(keyword);
|
||||
yield cleanup();
|
||||
});
|
||||
|
||||
add_task(function* test_setting_the_default_suggestion() {
|
||||
let keyword = "test";
|
||||
let extensionName = "Omnibox Example";
|
||||
|
||||
let mockExtension = {
|
||||
name: extensionName,
|
||||
emit(message, text, id) {
|
||||
if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
|
||||
ExtensionSearchHandler.addSuggestions(keyword, id, []);
|
||||
}
|
||||
controller.stopSearch();
|
||||
}
|
||||
};
|
||||
|
||||
ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
|
||||
|
||||
ExtensionSearchHandler.setDefaultSuggestion(keyword, {
|
||||
description: "hello world"
|
||||
});
|
||||
|
||||
let searchString = `${keyword} search query`;
|
||||
yield check_autocomplete({
|
||||
search: searchString,
|
||||
searchParam: "enable-actions",
|
||||
matches: [
|
||||
makeExtensionMatch({heuristic: true, keyword, description: "hello world", content: searchString}),
|
||||
]
|
||||
});
|
||||
|
||||
ExtensionSearchHandler.setDefaultSuggestion(keyword, {
|
||||
description: "foo bar"
|
||||
});
|
||||
|
||||
yield check_autocomplete({
|
||||
search: searchString,
|
||||
searchParam: "enable-actions",
|
||||
matches: [
|
||||
makeExtensionMatch({heuristic: true, keyword, description: "foo bar", content: searchString}),
|
||||
]
|
||||
});
|
||||
|
||||
ExtensionSearchHandler.unregisterKeyword(keyword);
|
||||
yield cleanup();
|
||||
});
|
||||
|
||||
add_task(function* test_maximum_number_of_suggestions_is_enforced() {
|
||||
let keyword = "test";
|
||||
let extensionName = "Omnibox Example";
|
||||
|
||||
let mockExtension = {
|
||||
name: extensionName,
|
||||
emit(message, text, id) {
|
||||
if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
|
||||
ExtensionSearchHandler.addSuggestions(keyword, id, [
|
||||
{content: "a", description: "first suggestion"},
|
||||
{content: "b", description: "second suggestion"},
|
||||
{content: "c", description: "third suggestion"},
|
||||
{content: "d", description: "fourth suggestion"},
|
||||
{content: "e", description: "fifth suggestion"},
|
||||
{content: "f", description: "sixth suggestion"},
|
||||
{content: "g", description: "seventh suggestion"},
|
||||
{content: "h", description: "eigth suggestion"},
|
||||
{content: "i", description: "ninth suggestion"},
|
||||
{content: "j", description: "tenth suggestion"},
|
||||
]);
|
||||
controller.stopSearch();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
|
||||
|
||||
// Start an input session before testing MSG_INPUT_CHANGED.
|
||||
ExtensionSearchHandler.handleSearch(keyword, `${keyword} `, () => {});
|
||||
|
||||
yield check_autocomplete({
|
||||
search: `${keyword} #`,
|
||||
searchParam: "enable-actions",
|
||||
matches: [
|
||||
makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} #`}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} a`, description: "first suggestion"}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} b`, description: "second suggestion"}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} c`, description: "third suggestion"}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} d`, description: "fourth suggestion"}),
|
||||
makeExtensionMatch({keyword, content: `${keyword} e`, description: "fifth suggestion"}),
|
||||
]
|
||||
});
|
||||
|
||||
ExtensionSearchHandler.unregisterKeyword(keyword);
|
||||
yield cleanup();
|
||||
});
|
@ -24,6 +24,7 @@ support-files =
|
||||
[test_empty_search.js]
|
||||
[test_enabled.js]
|
||||
[test_escape_self.js]
|
||||
[test_extension_matches.js]
|
||||
[test_ignore_protocol.js]
|
||||
[test_keyword_search.js]
|
||||
[test_keyword_search_actions.js]
|
||||
|
@ -2127,6 +2127,10 @@ extends="chrome://global/content/bindings/popup.xml#popup">
|
||||
titleLooksLikeUrl = true;
|
||||
let visitStr = this._stringBundle.GetStringFromName("visit");
|
||||
this._setUpDescription(this._actionText, visitStr, true);
|
||||
} else if (action.type == "extension") {
|
||||
let content = action.params.content;
|
||||
displayUrl = content;
|
||||
this._setUpDescription(this._actionText, content, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3497,11 +3497,18 @@ KeyboardLayout::NotifyIdleServiceOfUserActivity()
|
||||
sIdleService->ResetIdleTimeOut(0);
|
||||
}
|
||||
|
||||
KeyboardLayout::KeyboardLayout() :
|
||||
mKeyboardLayout(0), mIsOverridden(false),
|
||||
mIsPendingToRestoreKeyboardLayout(false)
|
||||
KeyboardLayout::KeyboardLayout()
|
||||
: mKeyboardLayout(0)
|
||||
, mIsOverridden(false)
|
||||
, mIsPendingToRestoreKeyboardLayout(false)
|
||||
{
|
||||
mDeadKeyTableListHead = nullptr;
|
||||
// A dead key sequence should be made from up to 5 keys. Therefore, 4 is
|
||||
// enough and makes sense because the item is uint8_t.
|
||||
// (Although, even if it's possible to be 6 keys or more in a sequence,
|
||||
// this array will be re-allocated).
|
||||
mActiveDeadKeys.SetCapacity(4);
|
||||
mDeadKeyShiftStates.SetCapacity(4);
|
||||
|
||||
// NOTE: LoadLayout() should be called via OnLayoutChange().
|
||||
}
|
||||
@ -3613,8 +3620,7 @@ KeyboardLayout::InitNativeKey(NativeKey& aNativeKey,
|
||||
// If it's in dead key sequence and dead char is inputted as is, we need to
|
||||
// set the previous modifier state which is stored when preceding dead key
|
||||
// is pressed.
|
||||
UniCharsAndModifiers deadChars =
|
||||
GetUniCharsAndModifiers(mActiveDeadKey, mDeadKeyShiftState);
|
||||
UniCharsAndModifiers deadChars = GetDeadUniCharsAndModifiers();
|
||||
aNativeKey.mCommittedCharsAndModifiers.
|
||||
OverwriteModifiersIfBeginsWith(deadChars);
|
||||
// Finish the dead key sequence.
|
||||
@ -3622,6 +3628,12 @@ KeyboardLayout::InitNativeKey(NativeKey& aNativeKey,
|
||||
return;
|
||||
}
|
||||
|
||||
// If it's a dead key, aNativeKey will be initialized by
|
||||
// MaybeInitNativeKeyAsDeadKey().
|
||||
if (MaybeInitNativeKeyAsDeadKey(aNativeKey, aModKeyState)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the key is not a usual printable key, KeyboardLayout class assume that
|
||||
// it's not cause dead char nor printable char. Therefore, there are nothing
|
||||
// to do here fore such keys (e.g., function keys).
|
||||
@ -3636,12 +3648,6 @@ KeyboardLayout::InitNativeKey(NativeKey& aNativeKey,
|
||||
MOZ_ASSERT(aNativeKey.mKeyNameIndex == KEY_NAME_INDEX_USE_STRING,
|
||||
"Printable key's key name index must be KEY_NAME_INDEX_USE_STRING");
|
||||
|
||||
// If it's a dead key, aNativeKey will be initialized by
|
||||
// MaybeInitNativeKeyAsDeadKey().
|
||||
if (MaybeInitNativeKeyAsDeadKey(aNativeKey, aModKeyState)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If it's in dead key handling and the pressed key causes a composite
|
||||
// character, aNativeKey will be initialized by
|
||||
// MaybeInitNativeKeyWithCompositeChar().
|
||||
@ -3659,19 +3665,10 @@ KeyboardLayout::InitNativeKey(NativeKey& aNativeKey,
|
||||
return;
|
||||
}
|
||||
|
||||
// Although, this shouldn't occur, if active dead key isn't a printable
|
||||
// key, we cannot handle it because KeyboardLayout assumes that dead key
|
||||
// is never mapped to non-printable keys (e.g., F4, etc). Please be aware,
|
||||
// it's possible, but we've not known such special keyboard layout yet.
|
||||
if (NS_WARN_IF(!IsPrintableCharKey(mActiveDeadKey))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the key doesn't cause a composite character with preceding dead key,
|
||||
// initialize aNativeKey with the dead-key character followed by current
|
||||
// key's character.
|
||||
UniCharsAndModifiers deadChars =
|
||||
GetUniCharsAndModifiers(mActiveDeadKey, mDeadKeyShiftState);
|
||||
UniCharsAndModifiers deadChars = GetDeadUniCharsAndModifiers();
|
||||
aNativeKey.mCommittedCharsAndModifiers = deadChars + baseChars;
|
||||
if (aNativeKey.IsKeyDownMessage()) {
|
||||
DeactivateDeadKeyState();
|
||||
@ -3683,25 +3680,28 @@ KeyboardLayout::MaybeInitNativeKeyAsDeadKey(
|
||||
NativeKey& aNativeKey,
|
||||
const ModifierKeyState& aModKeyState)
|
||||
{
|
||||
if (!IsDeadKey(aNativeKey.mOriginalVirtualKeyCode, aModKeyState)) {
|
||||
// Only when it's not in dead key sequence, we can trust IsDeadKey() result.
|
||||
if (!IsInDeadKeySequence() &&
|
||||
!IsDeadKey(aNativeKey.mOriginalVirtualKeyCode, aModKeyState)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If it's a keydown event but not in dead key sequence or it's a keyup
|
||||
// event of a dead key which activated current dead key sequence,
|
||||
// initialize aNativeKey as a dead key event.
|
||||
if ((aNativeKey.IsKeyDownMessage() && !IsInDeadKeySequence()) ||
|
||||
(!aNativeKey.IsKeyDownMessage() &&
|
||||
mActiveDeadKey == aNativeKey.mOriginalVirtualKeyCode)) {
|
||||
// When keydown message is followed by a dead char message, it should be
|
||||
// initialized as dead key.
|
||||
bool isDeadKeyDownEvent =
|
||||
aNativeKey.IsKeyDownMessage() &&
|
||||
aNativeKey.IsFollowedByDeadCharMessage();
|
||||
|
||||
// When keyup message is received, let's check if it's one of preceding
|
||||
// dead keys because keydown message order and keyup message order may be
|
||||
// different.
|
||||
bool isDeadKeyUpEvent =
|
||||
!aNativeKey.IsKeyDownMessage() &&
|
||||
mActiveDeadKeys.Contains(aNativeKey.mOriginalVirtualKeyCode);
|
||||
|
||||
if (isDeadKeyDownEvent || isDeadKeyUpEvent) {
|
||||
ActivateDeadKeyState(aNativeKey, aModKeyState);
|
||||
#ifdef DEBUG
|
||||
UniCharsAndModifiers deadChars =
|
||||
GetNativeUniCharsAndModifiers(aNativeKey.mOriginalVirtualKeyCode,
|
||||
aModKeyState);
|
||||
MOZ_ASSERT(deadChars.Length() == 1,
|
||||
"dead key must generate only one character");
|
||||
#endif
|
||||
// First dead key event doesn't generate characters. Dead key should
|
||||
// Any dead key events don't generate characters. So, a dead key should
|
||||
// cause only keydown event and keyup event whose KeyboardEvent.key
|
||||
// values are "Dead".
|
||||
aNativeKey.mCommittedCharsAndModifiers.Clear();
|
||||
@ -3720,27 +3720,18 @@ KeyboardLayout::MaybeInitNativeKeyAsDeadKey(
|
||||
return true;
|
||||
}
|
||||
|
||||
// When non-printable key event comes during a dead key sequence, that must
|
||||
// be a modifier key event. So, such events shouldn't be handled as a part
|
||||
// of the dead key sequence.
|
||||
if (!IsDeadKey(aNativeKey.mOriginalVirtualKeyCode, aModKeyState)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// FYI: Following code may run when the user doesn't input text actually
|
||||
// but the key sequence is a dead key sequence. For example,
|
||||
// ` -> Ctrl+` with Spanish keyboard layout. Let's keep using this
|
||||
// complicated code for now because this runs really rarely.
|
||||
|
||||
if (NS_WARN_IF(!IsPrintableCharKey(mActiveDeadKey))) {
|
||||
#if defined(DEBUG) || defined(MOZ_CRASHREPORTER)
|
||||
nsPrintfCString warning("The virtual key index (%d) of mActiveDeadKey "
|
||||
"(0x%02X) is not a printable key "
|
||||
"(aNativeKey.mOriginalVirtualKeyCode=0x%02X)",
|
||||
GetKeyIndex(mActiveDeadKey), mActiveDeadKey,
|
||||
aNativeKey.mOriginalVirtualKeyCode);
|
||||
NS_WARNING(warning.get());
|
||||
#ifdef MOZ_CRASHREPORTER
|
||||
CrashReporter::AppendAppNotesToCrashReport(
|
||||
NS_LITERAL_CSTRING("\n") + warning);
|
||||
#endif // #ifdef MOZ_CRASHREPORTER
|
||||
#endif // #if defined(DEBUG) || defined(MOZ_CRASHREPORTER)
|
||||
MOZ_CRASH("Trying to reference out of range of mVirtualKeys");
|
||||
}
|
||||
|
||||
// Dead key followed by another dead key may cause a composed character
|
||||
// (e.g., "Russian - Mnemonic" keyboard layout's 's' -> 'c').
|
||||
if (MaybeInitNativeKeyWithCompositeChar(aNativeKey, aModKeyState)) {
|
||||
@ -3749,8 +3740,7 @@ KeyboardLayout::MaybeInitNativeKeyAsDeadKey(
|
||||
|
||||
// Otherwise, dead key followed by another dead key causes inputting both
|
||||
// character.
|
||||
UniCharsAndModifiers prevDeadChars =
|
||||
GetUniCharsAndModifiers(mActiveDeadKey, mDeadKeyShiftState);
|
||||
UniCharsAndModifiers prevDeadChars = GetDeadUniCharsAndModifiers();
|
||||
UniCharsAndModifiers newChars =
|
||||
GetUniCharsAndModifiers(aNativeKey.mOriginalVirtualKeyCode, aModKeyState);
|
||||
// But keypress events should be fired for each committed character.
|
||||
@ -3770,8 +3760,7 @@ KeyboardLayout::MaybeInitNativeKeyWithCompositeChar(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (NS_WARN_IF(!IsPrintableCharKey(mActiveDeadKey)) ||
|
||||
NS_WARN_IF(!IsPrintableCharKey(aNativeKey.mOriginalVirtualKeyCode))) {
|
||||
if (NS_WARN_IF(!IsPrintableCharKey(aNativeKey.mOriginalVirtualKeyCode))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -3781,8 +3770,7 @@ KeyboardLayout::MaybeInitNativeKeyWithCompositeChar(
|
||||
return false;
|
||||
}
|
||||
|
||||
char16_t compositeChar =
|
||||
GetCompositeChar(mActiveDeadKey, mDeadKeyShiftState, baseChars.CharAt(0));
|
||||
char16_t compositeChar = GetCompositeChar(baseChars.CharAt(0));
|
||||
if (!compositeChar) {
|
||||
return false;
|
||||
}
|
||||
@ -3824,16 +3812,43 @@ KeyboardLayout::GetNativeUniCharsAndModifiers(
|
||||
return mVirtualKeys[key].GetNativeUniChars(shiftState);
|
||||
}
|
||||
|
||||
char16_t
|
||||
KeyboardLayout::GetCompositeChar(uint8_t aVirtualKeyOfDeadKey,
|
||||
VirtualKey::ShiftState aShiftStateOfDeadKey,
|
||||
char16_t aBaseChar) const
|
||||
UniCharsAndModifiers
|
||||
KeyboardLayout::GetDeadUniCharsAndModifiers() const
|
||||
{
|
||||
int32_t key = GetKeyIndex(aVirtualKeyOfDeadKey);
|
||||
MOZ_RELEASE_ASSERT(mActiveDeadKeys.Length() == mDeadKeyShiftStates.Length());
|
||||
|
||||
if (NS_WARN_IF(mActiveDeadKeys.IsEmpty())) {
|
||||
return UniCharsAndModifiers();
|
||||
}
|
||||
|
||||
UniCharsAndModifiers result;
|
||||
for (size_t i = 0; i < mActiveDeadKeys.Length(); ++i) {
|
||||
result +=
|
||||
GetUniCharsAndModifiers(mActiveDeadKeys[i], mDeadKeyShiftStates[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
char16_t
|
||||
KeyboardLayout::GetCompositeChar(char16_t aBaseChar) const
|
||||
{
|
||||
if (NS_WARN_IF(mActiveDeadKeys.IsEmpty())) {
|
||||
return 0;
|
||||
}
|
||||
// XXX Currently, we don't support computing a composite character with
|
||||
// two or more dead keys since it needs big table for supporting
|
||||
// long chained dead keys. However, this should be a minor bug
|
||||
// because this runs only when the latest keydown event does not cause
|
||||
// WM_(SYS)CHAR messages. So, when user wants to input a character,
|
||||
// this path never runs.
|
||||
if (mActiveDeadKeys.Length() > 1) {
|
||||
return 0;
|
||||
}
|
||||
int32_t key = GetKeyIndex(mActiveDeadKeys[0]);
|
||||
if (key < 0) {
|
||||
return 0;
|
||||
}
|
||||
return mVirtualKeys[key].GetCompositeChar(aShiftStateOfDeadKey, aBaseChar);
|
||||
return mVirtualKeys[key].GetCompositeChar(mDeadKeyShiftStates[0], aBaseChar);
|
||||
}
|
||||
|
||||
void
|
||||
@ -3860,7 +3875,8 @@ KeyboardLayout::LoadLayout(HKL aLayout)
|
||||
// characters.
|
||||
uint16_t shiftStatesWithBaseChars = 0;
|
||||
|
||||
mActiveDeadKey = -1;
|
||||
mActiveDeadKeys.Clear();
|
||||
mDeadKeyShiftStates.Clear();
|
||||
|
||||
ReleaseDeadKeyTables();
|
||||
|
||||
@ -4099,26 +4115,26 @@ KeyboardLayout::ActivateDeadKeyState(const NativeKey& aNativeKey,
|
||||
return;
|
||||
}
|
||||
|
||||
MOZ_RELEASE_ASSERT(IsPrintableCharKey(aNativeKey.mOriginalVirtualKeyCode));
|
||||
|
||||
mActiveDeadKey = aNativeKey.mOriginalVirtualKeyCode;
|
||||
mDeadKeyShiftState = VirtualKey::ModifierKeyStateToShiftState(aModKeyState);
|
||||
mActiveDeadKeys.AppendElement(aNativeKey.mOriginalVirtualKeyCode);
|
||||
mDeadKeyShiftStates.AppendElement(
|
||||
VirtualKey::ModifierKeyStateToShiftState(aModKeyState));
|
||||
}
|
||||
|
||||
void
|
||||
KeyboardLayout::DeactivateDeadKeyState()
|
||||
{
|
||||
if (mActiveDeadKey < 0) {
|
||||
if (mActiveDeadKeys.IsEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
BYTE kbdState[256];
|
||||
memset(kbdState, 0, sizeof(kbdState));
|
||||
|
||||
VirtualKey::FillKbdState(kbdState, mDeadKeyShiftState);
|
||||
|
||||
EnsureDeadKeyActive(false, mActiveDeadKey, kbdState);
|
||||
mActiveDeadKey = -1;
|
||||
// Assume that the last dead key can finish dead key sequence.
|
||||
VirtualKey::FillKbdState(kbdState, mDeadKeyShiftStates.LastElement());
|
||||
EnsureDeadKeyActive(false, mActiveDeadKeys.LastElement(), kbdState);
|
||||
mActiveDeadKeys.Clear();
|
||||
mDeadKeyShiftStates.Clear();
|
||||
}
|
||||
|
||||
bool
|
||||
|
@ -694,7 +694,7 @@ public:
|
||||
* It starts when a dead key is down and ends when another key down causes
|
||||
* inactivating the dead key state.
|
||||
*/
|
||||
bool IsInDeadKeySequence() const { return mActiveDeadKey >= 0; }
|
||||
bool IsInDeadKeySequence() const { return !mActiveDeadKeys.IsEmpty(); }
|
||||
|
||||
/**
|
||||
* IsSysKey() returns true if aVirtualKey with aModKeyState causes WM_SYSKEY*
|
||||
@ -811,8 +811,14 @@ private:
|
||||
|
||||
VirtualKey mVirtualKeys[NS_NUM_OF_KEYS];
|
||||
DeadKeyTableListEntry* mDeadKeyTableListHead;
|
||||
int32_t mActiveDeadKey; // -1 = no active dead-key
|
||||
VirtualKey::ShiftState mDeadKeyShiftState;
|
||||
// When mActiveDeadKeys is empty, it's not in dead key sequence.
|
||||
// Otherwise, it contains virtual keycodes which are pressed in current
|
||||
// dead key sequence.
|
||||
nsTArray<uint8_t> mActiveDeadKeys;
|
||||
// mDeadKeyShiftStates is always same length as mActiveDeadKeys.
|
||||
// This stores shift states at pressing each dead key stored in
|
||||
// mActiveDeadKeys.
|
||||
nsTArray<VirtualKey::ShiftState> mDeadKeyShiftStates;
|
||||
|
||||
bool mIsOverridden;
|
||||
bool mIsPendingToRestoreKeyboardLayout;
|
||||
@ -883,16 +889,20 @@ private:
|
||||
uint8_t aVirtualKey,
|
||||
VirtualKey::ShiftState aShiftState) const;
|
||||
|
||||
/**
|
||||
* GetDeadUniCharsAndModifiers() returns dead chars which are stored in
|
||||
* current dead key sequence. So, this is stateful.
|
||||
*/
|
||||
UniCharsAndModifiers GetDeadUniCharsAndModifiers() const;
|
||||
|
||||
/**
|
||||
* GetCompositeChar() returns a composite character with dead character
|
||||
* caused by aVirtualKeyOfDeadKey and aShiftStateOfDeadKey and a base
|
||||
* character (aBaseChar).
|
||||
* caused by mActiveDeadKeys, mDeadKeyShiftStates and a base character
|
||||
* (aBaseChar).
|
||||
* If the combination of the dead character and the base character doesn't
|
||||
* cause a composite character, this returns 0.
|
||||
*/
|
||||
char16_t GetCompositeChar(uint8_t aVirtualKeyOfDeadKey,
|
||||
VirtualKey::ShiftState aShiftStateOfDeadKey,
|
||||
char16_t aBaseChar) const;
|
||||
char16_t GetCompositeChar(char16_t aBaseChar) const;
|
||||
|
||||
// NativeKey class should access InitNativeKey() directly, but it shouldn't
|
||||
// be available outside of NativeKey. So, let's make NativeKey a friend
|
||||
|
Loading…
Reference in New Issue
Block a user