This commit is contained in:
Wes Kocher 2016-11-11 14:11:20 -08:00
Родитель 30fa2feaff cee41d5b80
Коммит 8dafd1ef34
71 изменённых файлов: 3777 добавлений и 443 удалений

Просмотреть файл

@ -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[

Просмотреть файл

@ -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(),
},
};
});

Просмотреть файл

@ -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

Просмотреть файл

@ -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]

Просмотреть файл

@ -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>

Просмотреть файл

@ -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
Просмотреть файл

@ -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
Просмотреть файл

@ -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
Просмотреть файл

@ -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
Просмотреть файл

@ -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

Просмотреть файл

@ -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;
};

Просмотреть файл

@ -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
Просмотреть файл

@ -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

Просмотреть файл

@ -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;

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -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 {

Просмотреть файл

@ -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