mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-01-26 23:23:33 +00:00
Merge m-c to inbound. a=merge
This commit is contained in:
commit
ef843a0e30
@ -53,11 +53,6 @@ GARBAGE += $(addprefix $(FINAL_TARGET)/defaults/pref/, firefox.js)
|
||||
|
||||
endif
|
||||
|
||||
# channel-prefs.js is handled separate from other prefs due to bug 756325
|
||||
libs:: $(srcdir)/profile/channel-prefs.js
|
||||
$(NSINSTALL) -D $(DIST)/bin/defaults/pref
|
||||
$(call py_action,preprocessor,-Fsubstitution $(PREF_PPFLAGS) $(ACDEFINES) $^ -o $(DIST)/bin/defaults/pref/channel-prefs.js)
|
||||
|
||||
ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT))
|
||||
|
||||
MAC_APP_NAME = $(MOZ_APP_DISPLAYNAME)
|
||||
|
@ -36,18 +36,15 @@ DIRS += ['profile/extensions']
|
||||
|
||||
GeckoProgram(CONFIG['MOZ_APP_NAME'])
|
||||
|
||||
JS_PREFERENCE_PP_FILES += [
|
||||
'profile/firefox.js',
|
||||
]
|
||||
|
||||
SOURCES += [
|
||||
'nsBrowserApp.cpp',
|
||||
]
|
||||
|
||||
FINAL_TARGET_FILES += ['blocklist.xml']
|
||||
FINAL_TARGET_FILES.defaults += ['permissions']
|
||||
# Neither channel-prefs.js nor firefox.exe want to end up in dist/bin/browser.
|
||||
DIST_SUBDIR = ""
|
||||
|
||||
DEFINES['APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
|
||||
# channel-prefs.js is handled separate from other prefs due to bug 756325
|
||||
JS_PREFERENCE_PP_FILES += ['profile/channel-prefs.js']
|
||||
|
||||
LOCAL_INCLUDES += [
|
||||
'!/build',
|
||||
@ -74,9 +71,6 @@ if CONFIG['OS_ARCH'] == 'WINNT':
|
||||
RCINCLUDE = 'splash.rc'
|
||||
DEFINES['MOZ_PHOENIX'] = True
|
||||
|
||||
for cdm in CONFIG['MOZ_EME_MODULES']:
|
||||
DEFINES['MOZ_%s_EME' % cdm.upper()] = True
|
||||
|
||||
if CONFIG['MOZ_SANDBOX'] and CONFIG['OS_ARCH'] == 'WINNT':
|
||||
# For sandbox includes and the include dependencies those have
|
||||
LOCAL_INCLUDES += [
|
||||
@ -112,9 +106,6 @@ if CONFIG['MOZ_LINKER']:
|
||||
if CONFIG['HAVE_CLOCK_MONOTONIC']:
|
||||
OS_LIBS += CONFIG['REALTIME_LIBS']
|
||||
|
||||
if CONFIG['MOZ_GPSD']:
|
||||
DEFINES['MOZ_GPSD'] = True
|
||||
|
||||
if CONFIG['MOZ_LINUX_32_SSE2_STARTUP_ERROR']:
|
||||
DEFINES['MOZ_LINUX_32_SSE2_STARTUP_ERROR'] = True
|
||||
COMPILE_FLAGS['OS_CXXFLAGS'] = [
|
||||
|
@ -2,4 +2,5 @@
|
||||
* 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/. */
|
||||
|
||||
#filter substitution
|
||||
pref("app.update.channel", "@MOZ_UPDATE_CHANNEL@");
|
||||
|
@ -48,24 +48,6 @@ var BrowserPageActions = {
|
||||
*/
|
||||
init() {
|
||||
this.placeAllActions();
|
||||
|
||||
// Add a click listener to #page-action-buttons for blocking clicks on
|
||||
// disabled actions in the urlbar. Normally we'd do this by setting
|
||||
// `pointer-events: none` in the CSS, but that also blocks context menu
|
||||
// events, and we want the context menu even on disabled actions so that
|
||||
// they can be removed from the urlbar.
|
||||
this.mainButtonNode.parentNode.addEventListener("click", event => {
|
||||
if (event.button == 2) {
|
||||
// Let context-clicks be handled normally.
|
||||
return;
|
||||
}
|
||||
let node = event.originalTarget;
|
||||
let action = this.actionForNode(node);
|
||||
if (action && action.getDisabled(window)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}, true);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -86,7 +68,7 @@ var BrowserPageActions = {
|
||||
// node.insertBefore() is always passed null, and nodes are always appended.
|
||||
// That will break the position of nodes that should be inserted before
|
||||
// nodes that are in markup, which in turn can break other nodes.
|
||||
let actionsInUrlbar = PageActions.actionsInUrlbar;
|
||||
let actionsInUrlbar = PageActions.actionsInUrlbar(window);
|
||||
for (let i = actionsInUrlbar.length - 1; i >= 0; i--) {
|
||||
let action = actionsInUrlbar[i];
|
||||
this.placeActionInUrlbar(action);
|
||||
@ -112,21 +94,19 @@ var BrowserPageActions = {
|
||||
* The action to place.
|
||||
*/
|
||||
placeActionInPanel(action) {
|
||||
let insertBeforeID = PageActions.nextActionIDInPanel(action);
|
||||
let id = this.panelButtonNodeIDForActionID(action.id);
|
||||
let node = document.getElementById(id);
|
||||
if (!node) {
|
||||
let panelViewNode;
|
||||
[node, panelViewNode] = this._makePanelButtonNodeForAction(action);
|
||||
node.id = id;
|
||||
let insertBeforeNode = null;
|
||||
if (insertBeforeID) {
|
||||
let insertBeforeNodeID =
|
||||
this.panelButtonNodeIDForActionID(insertBeforeID);
|
||||
insertBeforeNode = document.getElementById(insertBeforeNodeID);
|
||||
}
|
||||
let insertBeforeID = PageActions.nextActionIDInPanel(action);
|
||||
let insertBeforeNode =
|
||||
insertBeforeID ? this.panelButtonNodeForActionID(insertBeforeID) :
|
||||
null;
|
||||
this.mainViewBodyNode.insertBefore(node, insertBeforeNode);
|
||||
this.updateAction(action);
|
||||
this._updateActionDisabledInPanel(action);
|
||||
action.onPlacedInPanel(node);
|
||||
if (panelViewNode) {
|
||||
action.subview.onPlaced(panelViewNode);
|
||||
@ -356,11 +336,10 @@ var BrowserPageActions = {
|
||||
* The action to place.
|
||||
*/
|
||||
placeActionInUrlbar(action) {
|
||||
let insertBeforeID = PageActions.nextActionIDInUrlbar(action);
|
||||
let id = this.urlbarButtonNodeIDForActionID(action.id);
|
||||
let node = document.getElementById(id);
|
||||
|
||||
if (!action.shownInUrlbar) {
|
||||
if (!action.shouldShowInUrlbar(window)) {
|
||||
if (node) {
|
||||
if (action.__urlbarNodeInMarkup) {
|
||||
node.hidden = true;
|
||||
@ -382,14 +361,11 @@ var BrowserPageActions = {
|
||||
}
|
||||
|
||||
if (newlyPlaced) {
|
||||
let parentNode = this.mainButtonNode.parentNode;
|
||||
let insertBeforeNode = null;
|
||||
if (insertBeforeID) {
|
||||
let insertBeforeNodeID =
|
||||
this.urlbarButtonNodeIDForActionID(insertBeforeID);
|
||||
insertBeforeNode = document.getElementById(insertBeforeNodeID);
|
||||
}
|
||||
parentNode.insertBefore(node, insertBeforeNode);
|
||||
let insertBeforeID = PageActions.nextActionIDInUrlbar(window, action);
|
||||
let insertBeforeNode =
|
||||
insertBeforeID ? this.urlbarButtonNodeForActionID(insertBeforeID) :
|
||||
null;
|
||||
this.mainButtonNode.parentNode.insertBefore(node, insertBeforeNode);
|
||||
this.updateAction(action);
|
||||
action.onPlacedInUrlbar(node);
|
||||
|
||||
@ -398,8 +374,7 @@ var BrowserPageActions = {
|
||||
// button. Why not set tooltiptext to action.title when the node is
|
||||
// created? Because the consumer may set a title dynamically.
|
||||
if (!node.hasAttribute("tooltiptext")) {
|
||||
let panelNodeID = this.panelButtonNodeIDForActionID(action.id);
|
||||
let panelNode = document.getElementById(panelNodeID);
|
||||
let panelNode = this.panelButtonNodeForActionID(action.id);
|
||||
if (panelNode) {
|
||||
node.setAttribute("tooltiptext", panelNode.getAttribute("label"));
|
||||
}
|
||||
@ -412,9 +387,6 @@ var BrowserPageActions = {
|
||||
buttonNode.classList.add("urlbar-icon", "urlbar-page-action");
|
||||
buttonNode.setAttribute("actionid", action.id);
|
||||
buttonNode.setAttribute("role", "button");
|
||||
buttonNode.addEventListener("contextmenu", event => {
|
||||
BrowserPageActions.onContextMenu(event);
|
||||
});
|
||||
if (action.nodeAttributes) {
|
||||
for (let name in action.nodeAttributes) {
|
||||
buttonNode.setAttribute(name, action.nodeAttributes[name]);
|
||||
@ -439,8 +411,7 @@ var BrowserPageActions = {
|
||||
},
|
||||
|
||||
_removeActionFromPanel(action) {
|
||||
let id = this.panelButtonNodeIDForActionID(action.id);
|
||||
let node = document.getElementById(id);
|
||||
let node = this.panelButtonNodeForActionID(action.id);
|
||||
if (node) {
|
||||
node.remove();
|
||||
}
|
||||
@ -466,8 +437,7 @@ var BrowserPageActions = {
|
||||
},
|
||||
|
||||
_removeActionFromUrlbar(action) {
|
||||
let id = this.urlbarButtonNodeIDForActionID(action.id);
|
||||
let node = document.getElementById(id);
|
||||
let node = this.urlbarButtonNodeForActionID(action.id);
|
||||
if (node) {
|
||||
node.remove();
|
||||
}
|
||||
@ -479,56 +449,51 @@ var BrowserPageActions = {
|
||||
*
|
||||
* @param action (PageActions.Action, required)
|
||||
* The action to update.
|
||||
* @param nameToUpdate (string, optional)
|
||||
* The property's name. If not given, then DOM nodes will be updated
|
||||
* to reflect the current values of all properties.
|
||||
* @param propertyName (string, optional)
|
||||
* The name of the property to update. If not given, then DOM nodes
|
||||
* will be updated to reflect the current values of all properties.
|
||||
*/
|
||||
updateAction(action, nameToUpdate = null) {
|
||||
let names = nameToUpdate ? [nameToUpdate] : [
|
||||
"disabled",
|
||||
updateAction(action, propertyName = null) {
|
||||
let propertyNames = propertyName ? [propertyName] : [
|
||||
"iconURL",
|
||||
"title",
|
||||
"tooltip",
|
||||
];
|
||||
for (let name of names) {
|
||||
for (let name of propertyNames) {
|
||||
let upper = name[0].toUpperCase() + name.substr(1);
|
||||
this[`_updateAction${upper}`](action);
|
||||
}
|
||||
},
|
||||
|
||||
_updateActionDisabled(action) {
|
||||
let nodeIDs = [
|
||||
this.panelButtonNodeIDForActionID(action.id),
|
||||
this.urlbarButtonNodeIDForActionID(action.id),
|
||||
];
|
||||
for (let nodeID of nodeIDs) {
|
||||
let node = document.getElementById(nodeID);
|
||||
if (node) {
|
||||
if (action.getDisabled(window)) {
|
||||
node.setAttribute("disabled", "true");
|
||||
} else {
|
||||
node.removeAttribute("disabled");
|
||||
}
|
||||
this._updateActionDisabledInPanel(action);
|
||||
this.placeActionInUrlbar(action);
|
||||
},
|
||||
|
||||
_updateActionDisabledInPanel(action) {
|
||||
let panelButton = this.panelButtonNodeForActionID(action.id);
|
||||
if (panelButton) {
|
||||
if (action.getDisabled(window)) {
|
||||
panelButton.setAttribute("disabled", "true");
|
||||
} else {
|
||||
panelButton.removeAttribute("disabled");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_updateActionIconURL(action) {
|
||||
let nodeIDs = [
|
||||
this.panelButtonNodeIDForActionID(action.id),
|
||||
this.urlbarButtonNodeIDForActionID(action.id),
|
||||
];
|
||||
for (let nodeID of nodeIDs) {
|
||||
let node = document.getElementById(nodeID);
|
||||
if (node) {
|
||||
for (let size of [16, 32]) {
|
||||
let url = action.iconURLForSize(size, window);
|
||||
let prop = `--pageAction-image-${size}px`;
|
||||
if (url) {
|
||||
node.style.setProperty(prop, `url("${url}")`);
|
||||
} else {
|
||||
node.style.removeProperty(prop);
|
||||
}
|
||||
let nodes = [
|
||||
this.panelButtonNodeForActionID(action.id),
|
||||
this.urlbarButtonNodeForActionID(action.id),
|
||||
].filter(n => !!n);
|
||||
for (let node of nodes) {
|
||||
for (let size of [16, 32]) {
|
||||
let url = action.iconURLForSize(size, window);
|
||||
let prop = `--pageAction-image-${size}px`;
|
||||
if (url) {
|
||||
node.style.setProperty(prop, `url("${url}")`);
|
||||
} else {
|
||||
node.style.removeProperty(prop);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -543,13 +508,12 @@ var BrowserPageActions = {
|
||||
// return is to ignore that empty title.
|
||||
return;
|
||||
}
|
||||
let attrNamesByNodeIDFnName = {
|
||||
panelButtonNodeIDForActionID: "label",
|
||||
urlbarButtonNodeIDForActionID: "aria-label",
|
||||
let attrNamesByNodeFnName = {
|
||||
panelButtonNodeForActionID: "label",
|
||||
urlbarButtonNodeForActionID: "aria-label",
|
||||
};
|
||||
for (let [fnName, attrName] of Object.entries(attrNamesByNodeIDFnName)) {
|
||||
let nodeID = this[fnName](action.id);
|
||||
let node = document.getElementById(nodeID);
|
||||
for (let [fnName, attrName] of Object.entries(attrNamesByNodeFnName)) {
|
||||
let node = this[fnName](action.id);
|
||||
if (node) {
|
||||
node.setAttribute(attrName, title);
|
||||
}
|
||||
@ -559,9 +523,7 @@ var BrowserPageActions = {
|
||||
},
|
||||
|
||||
_updateActionTooltip(action) {
|
||||
let node = document.getElementById(
|
||||
this.urlbarButtonNodeIDForActionID(action.id)
|
||||
);
|
||||
let node = this.urlbarButtonNodeForActionID(action.id);
|
||||
if (node) {
|
||||
let tooltip = action.getTooltip(window) || action.getTitle(window);
|
||||
node.setAttribute("tooltiptext", tooltip);
|
||||
@ -575,8 +537,10 @@ var BrowserPageActions = {
|
||||
PageActions.logTelemetry("used", action, buttonNode);
|
||||
// If we're in the panel, open a subview inside the panel:
|
||||
// Note that we can't use this.panelNode.contains(buttonNode) here
|
||||
// because of XBL boundaries breaking ELement.contains.
|
||||
if (action.subview && buttonNode && buttonNode.closest("panel") == this.panelNode) {
|
||||
// because of XBL boundaries breaking Element.contains.
|
||||
if (action.subview &&
|
||||
buttonNode &&
|
||||
buttonNode.closest("panel") == this.panelNode) {
|
||||
let panelViewNodeID = this._panelViewNodeIDForActionID(action.id, false);
|
||||
let panelViewNode = document.getElementById(panelViewNodeID);
|
||||
action.subview.onShowing(panelViewNode);
|
||||
@ -627,6 +591,17 @@ var BrowserPageActions = {
|
||||
return action;
|
||||
},
|
||||
|
||||
/**
|
||||
* The given action's top-level button in the main panel.
|
||||
*
|
||||
* @param actionID (string, required)
|
||||
* The action ID.
|
||||
* @return (DOM node) The action's button in the main panel.
|
||||
*/
|
||||
panelButtonNodeForActionID(actionID) {
|
||||
return document.getElementById(this.panelButtonNodeIDForActionID(actionID));
|
||||
},
|
||||
|
||||
/**
|
||||
* The ID of the given action's top-level button in the main panel.
|
||||
*
|
||||
@ -638,6 +613,17 @@ var BrowserPageActions = {
|
||||
return `pageAction-panel-${actionID}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* The given action's button in the urlbar.
|
||||
*
|
||||
* @param actionID (string, required)
|
||||
* The action ID.
|
||||
* @return (DOM node) The action's urlbar button node.
|
||||
*/
|
||||
urlbarButtonNodeForActionID(actionID) {
|
||||
return document.getElementById(this.urlbarButtonNodeIDForActionID(actionID));
|
||||
},
|
||||
|
||||
/**
|
||||
* The ID of the given action's button in the urlbar.
|
||||
*
|
||||
@ -722,8 +708,7 @@ var BrowserPageActions = {
|
||||
*/
|
||||
showPanel(event = null) {
|
||||
for (let action of PageActions.actions) {
|
||||
let buttonNodeID = this.panelButtonNodeIDForActionID(action.id);
|
||||
let buttonNode = document.getElementById(buttonNodeID);
|
||||
let buttonNode = this.panelButtonNodeForActionID(action.id);
|
||||
action.onShowingInPanel(buttonNode);
|
||||
}
|
||||
|
||||
@ -738,22 +723,6 @@ var BrowserPageActions = {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Call this on the contextmenu event. Note that this is called before
|
||||
* onContextMenuShowing.
|
||||
*
|
||||
* @param event (DOM event, required)
|
||||
* The contextmenu event.
|
||||
*/
|
||||
onContextMenu(event) {
|
||||
let node = event.originalTarget;
|
||||
this._contextAction = this.actionForNode(node);
|
||||
// Don't show the menu if there's no action where the user clicked!
|
||||
if (!this._contextAction) {
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Call this on the context menu's popupshowing event.
|
||||
*
|
||||
@ -766,32 +735,58 @@ var BrowserPageActions = {
|
||||
if (event.target != popup) {
|
||||
return;
|
||||
}
|
||||
// Right now there's only one item in the context menu, to toggle the
|
||||
// context action's shown-in-urlbar state. Update it now.
|
||||
let toggleItem = popup.firstChild;
|
||||
let toggleItemLabel = null;
|
||||
if (this._contextAction) {
|
||||
toggleItem.disabled = false;
|
||||
if (this._contextAction.shownInUrlbar) {
|
||||
toggleItemLabel = toggleItem.getAttribute("remove-label");
|
||||
}
|
||||
|
||||
this._contextAction = this.actionForNode(popup.triggerNode);
|
||||
if (!this._contextAction) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (!toggleItemLabel) {
|
||||
toggleItemLabel = toggleItem.getAttribute("add-label");
|
||||
|
||||
let state;
|
||||
if (this._contextAction._isBuiltIn) {
|
||||
state =
|
||||
this._contextAction.pinnedToUrlbar ?
|
||||
"builtInPinned" :
|
||||
"builtInUnpinned";
|
||||
} else {
|
||||
state =
|
||||
this._contextAction.pinnedToUrlbar ?
|
||||
"extensionPinned" :
|
||||
"extensionUnpinned";
|
||||
}
|
||||
toggleItem.label = toggleItemLabel;
|
||||
popup.setAttribute("state", state);
|
||||
},
|
||||
|
||||
/**
|
||||
* Call this from the context menu's toggle menu item.
|
||||
* Call this from the menu item in the context menu that toggles pinning.
|
||||
*/
|
||||
toggleShownInUrlbarForContextAction() {
|
||||
togglePinningForContextAction() {
|
||||
if (!this._contextAction) {
|
||||
return;
|
||||
}
|
||||
let telemetryType = this._contextAction.shownInUrlbar ? "removed" : "added";
|
||||
PageActions.logTelemetry(telemetryType, this._contextAction);
|
||||
this._contextAction.shownInUrlbar = !this._contextAction.shownInUrlbar;
|
||||
let action = this._contextAction;
|
||||
this._contextAction = null;
|
||||
|
||||
let telemetryType = action.pinnedToUrlbar ? "removed" : "added";
|
||||
PageActions.logTelemetry(telemetryType, action);
|
||||
|
||||
action.pinnedToUrlbar = !action.pinnedToUrlbar;
|
||||
},
|
||||
|
||||
/**
|
||||
* Call this from the menu item in the context menu that opens about:addons.
|
||||
*/
|
||||
openAboutAddonsForContextAction() {
|
||||
if (!this._contextAction) {
|
||||
return;
|
||||
}
|
||||
let action = this._contextAction;
|
||||
this._contextAction = null;
|
||||
|
||||
PageActions.logTelemetry("managed", action);
|
||||
|
||||
let viewID = "addons://detail/" + encodeURIComponent(action.extensionID);
|
||||
window.BrowserOpenAddonsMgr(viewID);
|
||||
},
|
||||
|
||||
_contextAction: null,
|
||||
|
@ -1416,6 +1416,17 @@ toolbarpaletteitem[place="palette"][hidden] {
|
||||
}
|
||||
}
|
||||
|
||||
/* Page action context menu */
|
||||
#pageActionContextMenu > .pageActionContextMenuItem {
|
||||
visibility: collapse;
|
||||
}
|
||||
#pageActionContextMenu[state=builtInPinned] > .pageActionContextMenuItem.builtInPinned,
|
||||
#pageActionContextMenu[state=builtInUnpinned] > .pageActionContextMenuItem.builtInUnpinned,
|
||||
#pageActionContextMenu[state=extensionPinned] > .pageActionContextMenuItem.extensionPinned,
|
||||
#pageActionContextMenu[state=extensionUnpinned] > .pageActionContextMenuItem.extensionUnpinned {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* WebExtension Sidebars */
|
||||
#sidebar-box[sidebarcommand$="-sidebar-action"] > #sidebar-header > #sidebar-switcher-target > #sidebar-icon {
|
||||
list-style-image: var(--webextension-menuitem-image, inherit);
|
||||
|
@ -422,7 +422,6 @@
|
||||
viewCacheId="appMenu-viewCache">
|
||||
<panelview id="pageActionPanelMainView"
|
||||
context="pageActionContextMenu"
|
||||
oncontextmenu="BrowserPageActions.onContextMenu(event);"
|
||||
class="PanelUI-subView">
|
||||
<vbox class="panel-subview-body"/>
|
||||
</panelview>
|
||||
@ -446,11 +445,22 @@
|
||||
|
||||
<menupopup id="pageActionContextMenu"
|
||||
onpopupshowing="BrowserPageActions.onContextMenuShowing(event, this);">
|
||||
<menuitem id="pageActionContextMenu-toggleUrlbar"
|
||||
add-label="&pageAction.addToUrlbar.label;"
|
||||
remove-label="&pageAction.removeFromUrlbar.label;"
|
||||
<menuitem class="pageActionContextMenuItem builtInUnpinned"
|
||||
label="&pageAction.addToUrlbar.label;"
|
||||
oncommand="BrowserPageActions.toggleShownInUrlbarForContextAction();"/>
|
||||
oncommand="BrowserPageActions.togglePinningForContextAction();"/>
|
||||
<menuitem class="pageActionContextMenuItem builtInPinned"
|
||||
label="&pageAction.removeFromUrlbar.label;"
|
||||
oncommand="BrowserPageActions.togglePinningForContextAction();"/>
|
||||
<menuitem class="pageActionContextMenuItem extensionUnpinned"
|
||||
label="&pageAction.allowInUrlbar.label;"
|
||||
oncommand="BrowserPageActions.togglePinningForContextAction();"/>
|
||||
<menuitem class="pageActionContextMenuItem extensionPinned"
|
||||
label="&pageAction.disallowInUrlbar.label;"
|
||||
oncommand="BrowserPageActions.togglePinningForContextAction();"/>
|
||||
<menuseparator class="pageActionContextMenuItem extensionPinned extensionUnpinned"/>
|
||||
<menuitem class="pageActionContextMenuItem extensionPinned extensionUnpinned"
|
||||
label="&pageAction.manageExtension.label;"
|
||||
oncommand="BrowserPageActions.openAboutAddonsForContextAction();"/>
|
||||
</menupopup>
|
||||
|
||||
<!-- Bookmarks and history tooltip -->
|
||||
@ -859,9 +869,7 @@
|
||||
<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="page-action-buttons"
|
||||
context="pageActionContextMenu"
|
||||
oncontextmenu="BrowserPageActions.onContextMenu(event);">
|
||||
<hbox id="page-action-buttons" context="pageActionContextMenu">
|
||||
<hbox id="userContext-icons" hidden="true">
|
||||
<label id="userContext-label"/>
|
||||
<image id="userContext-indicator"/>
|
||||
|
@ -289,6 +289,7 @@
|
||||
<radio class="permPluginTemplateRadioDefault" label="&permUseDefault;"/>
|
||||
<radio class="permPluginTemplateRadioAsk" label="&permAskAlways;"/>
|
||||
<radio class="permPluginTemplateRadioAllow" label="&permAllow;"/>
|
||||
<radio class="permPluginTemplateRadioHide" label="&permHide;"/>
|
||||
<radio class="permPluginTemplateRadioBlock" label="&permBlock;"/>
|
||||
</radiogroup>
|
||||
</hbox>
|
||||
|
@ -183,6 +183,8 @@ function fillInPluginPermissionTemplate(aPluginName, aPermissionString) {
|
||||
[ ".permPluginTemplateRadioDefault", "id", aPermissionString + "#0" ],
|
||||
[ ".permPluginTemplateRadioAsk", "id", aPermissionString + "#3" ],
|
||||
[ ".permPluginTemplateRadioAllow", "id", aPermissionString + "#1" ],
|
||||
// #8 comes from Ci.nsIObjectLoadingContent.PLUGIN_PERMISSION_PROMPT_ACTION_QUIET
|
||||
[ ".permPluginTemplateRadioHide", "id", aPermissionString + "#8"],
|
||||
[ ".permPluginTemplateRadioBlock", "id", aPermissionString + "#2" ]
|
||||
];
|
||||
|
||||
|
@ -26,6 +26,7 @@ support-files =
|
||||
plugin_hidden_to_visible.html
|
||||
plugin_iframe.html
|
||||
plugin_outsideScrollArea.html
|
||||
plugin_overlay_styles.html
|
||||
plugin_overlayed.html
|
||||
plugin_positioned.html
|
||||
plugin_simple_blank.swf
|
||||
@ -71,6 +72,7 @@ tags = blocklist
|
||||
tags = blocklist
|
||||
[browser_CTP_outsideScrollArea.js]
|
||||
tags = blocklist
|
||||
[browser_CTP_overlay_styles.js]
|
||||
[browser_CTP_remove_navigate.js]
|
||||
tags = blocklist
|
||||
[browser_CTP_resize.js]
|
||||
|
@ -82,8 +82,8 @@ add_task(async function() {
|
||||
let doc = content.document;
|
||||
let plugin = doc.getElementById("test");
|
||||
let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
|
||||
Assert.ok(!overlay || !overlay.classList.contains("visible"),
|
||||
"Test 3b, overlay should be hidden.");
|
||||
Assert.ok(!overlay || overlay.getAttribute("sizing") == "blank",
|
||||
"Test 3b, overlay should be blank.");
|
||||
});
|
||||
});
|
||||
|
||||
@ -109,8 +109,8 @@ add_task(async function() {
|
||||
let doc = content.document;
|
||||
let plugin = doc.getElementById("test");
|
||||
let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
|
||||
Assert.ok(!overlay || !overlay.classList.contains("visible"),
|
||||
"Test 4b, overlay should be hidden.");
|
||||
Assert.ok(!overlay || overlay.getAttribute("sizing") == "blank",
|
||||
"Test 4b, overlay should be blank.");
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -58,7 +58,7 @@ add_task(async function() {
|
||||
let doc = content.document;
|
||||
let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
|
||||
Assert.ok(overlay && overlay.classList.contains("visible") &&
|
||||
!overlay.classList.contains("minimal"),
|
||||
overlay.getAttribute("sizing") != "blank",
|
||||
"Test 2, overlay should be visible.");
|
||||
});
|
||||
});
|
||||
@ -88,7 +88,7 @@ add_task(async function() {
|
||||
let doc = content.document;
|
||||
let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
|
||||
Assert.ok(overlay && overlay.classList.contains("visible") &&
|
||||
!overlay.classList.contains("minimal"),
|
||||
overlay.getAttribute("sizing") != "blank",
|
||||
"Test 3, overlay should be visible.");
|
||||
});
|
||||
});
|
||||
@ -116,7 +116,7 @@ add_task(async function() {
|
||||
let plugin = content.document.getElementById("test");
|
||||
let doc = content.document;
|
||||
let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
|
||||
Assert.ok(!overlay || !overlay.classList.contains("visible"),
|
||||
"Test 4, overlay should be hidden.");
|
||||
Assert.ok(!overlay || overlay.getAttribute("sizing") == "blank",
|
||||
"Test 4, overlay should be blank.");
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,93 @@
|
||||
/* 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 test ensures that the click-to-play "Activate Plugin" overlay
|
||||
* is shown in the right style (which is dependent on its size).
|
||||
*/
|
||||
|
||||
const rootDir = getRootDirectory(gTestPath);
|
||||
const gTestRoot = rootDir.replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
|
||||
|
||||
var gTestBrowser = null;
|
||||
|
||||
const gTestcases = {
|
||||
|
||||
// 10x10
|
||||
testcase1: {
|
||||
sizing: "blank",
|
||||
notext: null,
|
||||
},
|
||||
|
||||
// 40x40
|
||||
testcase2: {
|
||||
sizing: "tiny",
|
||||
notext: "notext",
|
||||
},
|
||||
|
||||
// 100x70
|
||||
testcase3: {
|
||||
sizing: "reduced",
|
||||
notext: "notext",
|
||||
},
|
||||
|
||||
// 200x200
|
||||
testcase4: {
|
||||
sizing: null,
|
||||
notext: "notext",
|
||||
},
|
||||
|
||||
// 300x300
|
||||
testcase5: {
|
||||
sizing: null,
|
||||
notext: null,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
add_task(async function() {
|
||||
registerCleanupFunction(function() {
|
||||
clearAllPluginPermissions();
|
||||
setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
|
||||
gBrowser.removeCurrentTab();
|
||||
gTestBrowser = null;
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function() {
|
||||
gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
|
||||
gTestBrowser = gBrowser.selectedBrowser;
|
||||
|
||||
setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Test Plug-in");
|
||||
|
||||
let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
|
||||
ok(!popupNotification, "Sanity check, should not have a click-to-play notification");
|
||||
|
||||
await promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_overlay_styles.html");
|
||||
|
||||
// Work around for delayed PluginBindingAttached
|
||||
await promiseUpdatePluginBindings(gTestBrowser);
|
||||
|
||||
await ContentTask.spawn(gTestBrowser, gTestcases, async function(testcases) {
|
||||
let doc = content.document;
|
||||
|
||||
for (let testcaseId of Object.keys(testcases)) {
|
||||
let plugin = doc.querySelector(`#${testcaseId} > object`);
|
||||
let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
|
||||
Assert.ok(overlay, `overlay exists in ${testcaseId}`);
|
||||
|
||||
let expectations = testcases[testcaseId];
|
||||
|
||||
Assert.ok(overlay.classList.contains("visible") == true,
|
||||
`The expected visibility is correct in ${testcaseId}`);
|
||||
|
||||
Assert.ok(overlay.getAttribute("sizing") == expectations.sizing,
|
||||
`The expected sizing is correct in ${testcaseId}`);
|
||||
|
||||
Assert.ok(overlay.getAttribute("notext") == expectations.notext,
|
||||
`The expected notext is correct in ${testcaseId}`);
|
||||
}
|
||||
});
|
||||
});
|
@ -46,8 +46,8 @@ add_task(async function() {
|
||||
let doc = content.document;
|
||||
let plugin = doc.getElementById("test");
|
||||
let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
|
||||
Assert.ok(!overlay || overlay.classList.contains("minimal"),
|
||||
"Test 2, overlay should be hidden.");
|
||||
Assert.ok(!overlay || overlay.getAttribute("sizing") == "blank",
|
||||
"Test 2, overlay should be blank.");
|
||||
});
|
||||
});
|
||||
|
||||
@ -64,8 +64,8 @@ add_task(async function() {
|
||||
let doc = content.document;
|
||||
let plugin = doc.getElementById("test");
|
||||
let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
|
||||
Assert.ok(!overlay || overlay.classList.contains("minimal"),
|
||||
"Test 3, overlay should be hidden.");
|
||||
Assert.ok(!overlay || overlay.getAttribute("sizing") == "blank",
|
||||
"Test 3, overlay should be blank.");
|
||||
});
|
||||
});
|
||||
|
||||
@ -84,7 +84,7 @@ add_task(async function() {
|
||||
let doc = content.document;
|
||||
let plugin = doc.getElementById("test");
|
||||
let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
|
||||
Assert.ok(overlay && overlay.classList.contains("visible"),
|
||||
Assert.ok(overlay && overlay.getAttribute("sizing") != "blank",
|
||||
"Test 4, overlay should be visible.");
|
||||
});
|
||||
});
|
||||
@ -104,8 +104,8 @@ add_task(async function() {
|
||||
let doc = content.document;
|
||||
let plugin = doc.getElementById("test");
|
||||
let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
|
||||
Assert.ok(!overlay || overlay.classList.contains("minimal"),
|
||||
"Test 5, overlay should be hidden.");
|
||||
Assert.ok(!overlay || overlay.getAttribute("sizing") == "blank",
|
||||
"Test 5, overlay should be blank.");
|
||||
});
|
||||
});
|
||||
|
||||
@ -124,7 +124,7 @@ add_task(async function() {
|
||||
let doc = content.document;
|
||||
let plugin = doc.getElementById("test");
|
||||
let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
|
||||
Assert.ok(overlay && overlay.classList.contains("visible"),
|
||||
Assert.ok(overlay && overlay.getAttribute("sizing") != "blank",
|
||||
"Test 6, overlay should be visible.");
|
||||
});
|
||||
});
|
||||
|
@ -7,6 +7,12 @@
|
||||
/* This test ensures that the click-to-play "Activate Plugin" overlay
|
||||
* is shown when expected.
|
||||
* All testcases are in the plugin_shouldShowOverlay.html file.
|
||||
*
|
||||
* Note: Technically, the overlay is *always* shown. When this test was
|
||||
* originally written, the meaning of "shown" was "shown with the contents",
|
||||
* as opposed to "shown as blank". The behavior hasn't changed, but the naming
|
||||
* has: now, a "shown as blank" overlay no longer receives a ".hidden" class.
|
||||
* It receives a sizing="blank" attribute.
|
||||
*/
|
||||
|
||||
var rootDir = getRootDirectory(gTestPath);
|
||||
@ -49,7 +55,7 @@ add_task(async function() {
|
||||
Assert.ok(overlay, `overlay exists in ${testcase.id}`);
|
||||
|
||||
let expectedVisibility = (testcase.getAttribute("shouldshow") == "true");
|
||||
Assert.ok(overlay.classList.contains("visible") == expectedVisibility,
|
||||
Assert.ok((overlay.getAttribute("sizing") != "blank") == expectedVisibility,
|
||||
`The expected visibility is correct in ${testcase.id}`);
|
||||
}
|
||||
})
|
||||
|
29
browser/base/content/test/plugins/plugin_overlay_styles.html
Normal file
29
browser/base/content/test/plugins/plugin_overlay_styles.html
Normal file
@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="testcase1" class="testcase">
|
||||
<object width="10" height="10" type="application/x-test"></object>
|
||||
</div>
|
||||
|
||||
<div id="testcase2" class="testcase">
|
||||
<object width="40" height="40" type="application/x-test"></object>
|
||||
</div>
|
||||
|
||||
<div id="testcase3" class="testcase">
|
||||
<object width="100" height="70" type="application/x-test"></object>
|
||||
</div>
|
||||
|
||||
<div id="testcase4" class="testcase">
|
||||
<object width="200" height="200" type="application/x-test"></object>
|
||||
</div>
|
||||
|
||||
<div id="testcase5" class="testcase">
|
||||
<object width="300" height="300" type="application/x-test"></object>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -4,6 +4,6 @@
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
<embed id="test" style="width: 200px; height: 200px" type="application/x-test">
|
||||
<embed id="test" style="width: 300px; height: 300px" type="application/x-test">
|
||||
</body>
|
||||
</html>
|
||||
|
@ -122,9 +122,6 @@ var whitelist = [
|
||||
{file: "resource://shield-recipe-client-content/shield-content-frame.js"},
|
||||
{file: "resource://shield-recipe-client-content/shield-content-process.js"},
|
||||
|
||||
// New L10n API that is not yet used in production
|
||||
{file: "chrome://global/content/l10n.js"},
|
||||
|
||||
// Starting from here, files in the whitelist are bugs that need fixing.
|
||||
// Bug 1339424 (wontfix?)
|
||||
{file: "chrome://browser/locale/taskbar.properties",
|
||||
|
@ -157,8 +157,8 @@ add_task(async function copyURLFromURLBar() {
|
||||
await BrowserTestUtils.withNewTab(url, async () => {
|
||||
// Add action to URL bar.
|
||||
let action = PageActions._builtInActions.find(a => a.id == "copyURL");
|
||||
action.shownInUrlbar = true;
|
||||
registerCleanupFunction(() => action.shownInUrlbar = false);
|
||||
action.pinnedToUrlbar = true;
|
||||
registerCleanupFunction(() => action.pinnedToUrlbar = false);
|
||||
|
||||
let copyURLButton =
|
||||
document.getElementById("pageAction-urlbar-copyURL");
|
||||
@ -544,7 +544,7 @@ add_task(async function sendToDevice_inUrlbar() {
|
||||
|
||||
// Add Send to Device to the urlbar.
|
||||
let action = PageActions.actionForID("sendToDevice");
|
||||
action.shownInUrlbar = true;
|
||||
action.pinnedToUrlbar = true;
|
||||
|
||||
// Click it to open its panel.
|
||||
let urlbarButton = document.getElementById(
|
||||
@ -622,7 +622,7 @@ add_task(async function sendToDevice_inUrlbar() {
|
||||
await promisePanelHidden(BrowserPageActionFeedback.panelNode.id);
|
||||
|
||||
// Remove Send to Device from the urlbar.
|
||||
action.shownInUrlbar = false;
|
||||
action.pinnedToUrlbar = false;
|
||||
|
||||
cleanUp();
|
||||
});
|
||||
@ -642,14 +642,14 @@ add_task(async function contextMenu() {
|
||||
});
|
||||
await contextMenuPromise;
|
||||
|
||||
// The context menu should show "Remove from Address Bar". Click it.
|
||||
let contextMenuNode = document.getElementById("pageActionContextMenu");
|
||||
Assert.equal(contextMenuNode.childNodes.length, 1,
|
||||
// The context menu should show the "remove" item. Click it.
|
||||
let menuItems = collectContextMenuItems();
|
||||
Assert.equal(menuItems.length, 1,
|
||||
"Context menu has one child");
|
||||
Assert.equal(contextMenuNode.childNodes[0].label, "Remove from Address Bar",
|
||||
Assert.equal(menuItems[0].label, "Remove from Address Bar",
|
||||
"Context menu is in the 'remove' state");
|
||||
contextMenuPromise = promisePanelHidden("pageActionContextMenu");
|
||||
EventUtils.synthesizeMouseAtCenter(contextMenuNode.childNodes[0], {});
|
||||
EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
|
||||
await contextMenuPromise;
|
||||
|
||||
// The action should be removed from the urlbar. In this case, the bookmark
|
||||
@ -668,13 +668,14 @@ add_task(async function contextMenu() {
|
||||
});
|
||||
await contextMenuPromise;
|
||||
|
||||
// The context menu should show "Add to Address Bar". Click it.
|
||||
Assert.equal(contextMenuNode.childNodes.length, 1,
|
||||
// The context menu should show the "add" item. Click it.
|
||||
menuItems = collectContextMenuItems();
|
||||
Assert.equal(menuItems.length, 1,
|
||||
"Context menu has one child");
|
||||
Assert.equal(contextMenuNode.childNodes[0].label, "Add to Address Bar",
|
||||
Assert.equal(menuItems[0].label, "Add to Address Bar",
|
||||
"Context menu is in the 'add' state");
|
||||
contextMenuPromise = promisePanelHidden("pageActionContextMenu");
|
||||
EventUtils.synthesizeMouseAtCenter(contextMenuNode.childNodes[0], {});
|
||||
EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
|
||||
await contextMenuPromise;
|
||||
|
||||
// The action should be added to the urlbar.
|
||||
@ -690,13 +691,14 @@ add_task(async function contextMenu() {
|
||||
});
|
||||
await contextMenuPromise;
|
||||
|
||||
// The context menu should show "Remove from Address Bar". Click it.
|
||||
Assert.equal(contextMenuNode.childNodes.length, 1,
|
||||
// The context menu should show the "remove" item. Click it.
|
||||
menuItems = collectContextMenuItems();
|
||||
Assert.equal(menuItems.length, 1,
|
||||
"Context menu has one child");
|
||||
Assert.equal(contextMenuNode.childNodes[0].label, "Remove from Address Bar",
|
||||
Assert.equal(menuItems[0].label, "Remove from Address Bar",
|
||||
"Context menu is in the 'remove' state");
|
||||
contextMenuPromise = promisePanelHidden("pageActionContextMenu");
|
||||
EventUtils.synthesizeMouseAtCenter(contextMenuNode.childNodes[0], {});
|
||||
EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
|
||||
await contextMenuPromise;
|
||||
|
||||
// The action should be removed from the urlbar.
|
||||
@ -713,12 +715,14 @@ add_task(async function contextMenu() {
|
||||
button: 2,
|
||||
});
|
||||
await contextMenuPromise;
|
||||
Assert.equal(contextMenuNode.childNodes.length, 1,
|
||||
|
||||
menuItems = collectContextMenuItems();
|
||||
Assert.equal(menuItems.length, 1,
|
||||
"Context menu has one child");
|
||||
Assert.equal(contextMenuNode.childNodes[0].label, "Add to Address Bar",
|
||||
Assert.equal(menuItems[0].label, "Add to Address Bar",
|
||||
"Context menu is in the 'add' state");
|
||||
contextMenuPromise = promisePanelHidden("pageActionContextMenu");
|
||||
EventUtils.synthesizeMouseAtCenter(contextMenuNode.childNodes[0], {});
|
||||
EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
|
||||
await contextMenuPromise;
|
||||
await BrowserTestUtils.waitForCondition(() => {
|
||||
return !starButtonBox.hidden;
|
||||
@ -775,3 +779,10 @@ function checkSendToDeviceItems(expectedItems, forUrlbar = false) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function collectContextMenuItems() {
|
||||
let contextMenu = document.getElementById("pageActionContextMenu");
|
||||
return Array.filter(contextMenu.childNodes, node => {
|
||||
return window.getComputedStyle(node).visibility == "visible";
|
||||
});
|
||||
}
|
||||
|
@ -66,9 +66,10 @@ this.pageAction = class extends ExtensionAPI {
|
||||
if (!this.browserPageAction) {
|
||||
this.browserPageAction = PageActions.addAction(new PageActions.Action({
|
||||
id: widgetId,
|
||||
extensionID: extension.id,
|
||||
title: this.defaults.title,
|
||||
iconURL: this.getIconData(this.defaults.icon),
|
||||
shownInUrlbar: true,
|
||||
pinnedToUrlbar: true,
|
||||
disabled: true,
|
||||
onCommand: (event, buttonNode) => {
|
||||
this.handleClick(event.target.ownerGlobal);
|
||||
|
@ -24,7 +24,7 @@ function makeWidgetId(id) {
|
||||
return id.replace(/[^a-z0-9_-]/g, "_");
|
||||
}
|
||||
|
||||
// Tests that the shownInUrlbar property of the PageActions.Action object
|
||||
// Tests that the pinnedToUrlbar property of the PageActions.Action object
|
||||
// backing the extension's page action persists across app restarts.
|
||||
add_task(async function testAppShutdown() {
|
||||
let extensionData = {
|
||||
@ -44,11 +44,11 @@ add_task(async function testAppShutdown() {
|
||||
let extension = ExtensionTestUtils.loadExtension(extensionData);
|
||||
await extension.startup();
|
||||
|
||||
// Get the PageAction.Action object. Its shownInUrlbar should have been
|
||||
// Get the PageAction.Action object. Its pinnedToUrlbar should have been
|
||||
// initialized to true in ext-pageAction.js, when it's created.
|
||||
let actionID = makeWidgetId(extension.id);
|
||||
let action = PageActions.actionForID(actionID);
|
||||
Assert.equal(action.shownInUrlbar, true);
|
||||
Assert.equal(action.pinnedToUrlbar, true);
|
||||
|
||||
// Simulate restarting the app without first unloading the extension.
|
||||
await promiseShutdownManager();
|
||||
@ -56,12 +56,12 @@ add_task(async function testAppShutdown() {
|
||||
await promiseStartupManager();
|
||||
await extension.awaitStartup();
|
||||
|
||||
// Get the action. Its shownInUrlbar should remain true.
|
||||
// Get the action. Its pinnedToUrlbar should remain true.
|
||||
action = PageActions.actionForID(actionID);
|
||||
Assert.equal(action.shownInUrlbar, true);
|
||||
Assert.equal(action.pinnedToUrlbar, true);
|
||||
|
||||
// Now set its shownInUrlbar to false.
|
||||
action.shownInUrlbar = false;
|
||||
// Now set its pinnedToUrlbar to false.
|
||||
action.pinnedToUrlbar = false;
|
||||
|
||||
// Simulate restarting the app again without first unloading the extension.
|
||||
await promiseShutdownManager();
|
||||
@ -69,9 +69,9 @@ add_task(async function testAppShutdown() {
|
||||
await promiseStartupManager();
|
||||
await extension.awaitStartup();
|
||||
|
||||
// Get the action. Its shownInUrlbar should remain false.
|
||||
// Get the action. Its pinnedToUrlbar should remain false.
|
||||
action = PageActions.actionForID(actionID);
|
||||
Assert.equal(action.shownInUrlbar, false);
|
||||
Assert.equal(action.pinnedToUrlbar, false);
|
||||
|
||||
// Now unload the extension and quit the app.
|
||||
await extension.unload();
|
||||
|
@ -1186,12 +1186,28 @@ var gMainPane = {
|
||||
let processCountPref = document.getElementById("dom.ipc.processCount");
|
||||
let defaultProcessCount = processCountPref.defaultValue;
|
||||
let bundlePreferences = document.getElementById("bundlePreferences");
|
||||
let label = bundlePreferences.getFormattedString("defaultContentProcessCount",
|
||||
[defaultProcessCount]);
|
||||
|
||||
let contentProcessCount =
|
||||
document.querySelector(`#contentProcessCount > menupopup >
|
||||
menuitem[value="${defaultProcessCount}"]`);
|
||||
contentProcessCount.label = label;
|
||||
|
||||
// New localization API experiment (October 2017).
|
||||
// See bug 1402061 for details.
|
||||
//
|
||||
// The `intl.l10n.fluent.disabled` pref can be used
|
||||
// to opt-out of the experiment in case of any
|
||||
// unforseen problems. The legacy API will then
|
||||
// be used.
|
||||
if (Services.prefs.getBoolPref("intl.l10n.fluent.disabled", false)) {
|
||||
let label = bundlePreferences.getFormattedString("defaultContentProcessCount",
|
||||
[defaultProcessCount]);
|
||||
contentProcessCount.label = label;
|
||||
} else {
|
||||
document.l10n.setAttributes(
|
||||
contentProcessCount,
|
||||
"default-content-process-count",
|
||||
{ num: defaultProcessCount });
|
||||
}
|
||||
|
||||
document.getElementById("limitContentProcess").disabled = false;
|
||||
document.getElementById("contentProcessCount").disabled = false;
|
||||
|
@ -4,6 +4,9 @@
|
||||
|
||||
<!-- General panel -->
|
||||
|
||||
<link rel="localization" href="browser/preferences/main.ftl"/>
|
||||
<script type="text/javascript" src="chrome://global/content/l10n.js"></script>
|
||||
|
||||
<script type="application/javascript"
|
||||
src="chrome://browser/content/preferences/in-content/main.js"/>
|
||||
|
||||
|
@ -176,7 +176,9 @@
|
||||
|
||||
<hbox class="help-button" pack="center">
|
||||
<label class="text-link">
|
||||
<image class="help-icon"/><label class="help-label" flex="1">&helpButton2.label;</label>
|
||||
<hbox align="center">
|
||||
<image class="help-icon"/><label class="help-label" flex="1">&helpButton2.label;</label>
|
||||
</hbox>
|
||||
</label>
|
||||
</hbox>
|
||||
</vbox>
|
||||
|
@ -85,3 +85,4 @@ support-files =
|
||||
skip-if = !healthreport || !telemetry || (os == 'linux' && debug) || (os == 'android')
|
||||
[browser_containers_name_input.js]
|
||||
run-if = nightly_build # Containers is enabled only on Nightly
|
||||
[browser_fluent.js]
|
||||
|
@ -0,0 +1,48 @@
|
||||
/* eslint-disable mozilla/no-cpows-in-tests */
|
||||
|
||||
function whenMainPaneLoadedFinished() {
|
||||
return new Promise(function(resolve, reject) {
|
||||
const topic = "main-pane-loaded";
|
||||
Services.obs.addObserver(function observer(aSubject) {
|
||||
Services.obs.removeObserver(observer, topic);
|
||||
resolve();
|
||||
}, topic);
|
||||
});
|
||||
}
|
||||
|
||||
// Temporary test for an experimental new localization API.
|
||||
// See bug 1402069 for details.
|
||||
add_task(async function() {
|
||||
// The string is used only when `browserTabsRemoteAutostart` is true
|
||||
if (!Services.appinfo.browserTabsRemoteAutostart) {
|
||||
ok(true, "fake test to avoid harness complaining");
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
openPreferencesViaOpenPreferencesAPI("general", {leaveOpen: true}),
|
||||
whenMainPaneLoadedFinished(),
|
||||
]);
|
||||
|
||||
let doc = gBrowser.contentDocument;
|
||||
await doc.l10n.ready;
|
||||
|
||||
let processCountPref = doc.getElementById("dom.ipc.processCount");
|
||||
let defaultProcessCount = processCountPref.defaultValue;
|
||||
|
||||
let [ msg ] = await doc.l10n.formatMessages([
|
||||
["default-content-process-count", { num: defaultProcessCount }]
|
||||
]);
|
||||
|
||||
let elem = doc.querySelector(
|
||||
`#contentProcessCount > menupopup > menuitem[value="${defaultProcessCount}"]`);
|
||||
|
||||
Assert.deepEqual(msg, {
|
||||
value: null,
|
||||
attrs: [
|
||||
["label", elem.getAttribute("label")]
|
||||
]
|
||||
});
|
||||
|
||||
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
|
||||
});
|
@ -136,8 +136,8 @@ var pageActionsHelper = {
|
||||
setActionsUrlbarState(inUrlbar) {
|
||||
this._originalStates = [];
|
||||
PageActions._actionsByID.forEach(action => {
|
||||
this._originalStates.push([ action, action.shownInUrlbar ]);
|
||||
action.shownInUrlbar = inUrlbar;
|
||||
this._originalStates.push([ action, action.pinnedToUrlbar ]);
|
||||
action.pinnedToUrlbar = inUrlbar;
|
||||
});
|
||||
},
|
||||
|
||||
@ -146,7 +146,7 @@ var pageActionsHelper = {
|
||||
return;
|
||||
}
|
||||
for (let [ action, originalState] of this._originalStates) {
|
||||
action.shownInUrlbar = originalState;
|
||||
action.pinnedToUrlbar = originalState;
|
||||
}
|
||||
this._originalStates = null;
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ const CONTENT = {
|
||||
},
|
||||
mainAction: {
|
||||
label: GetStringFromName(changeAutofillOptsKey),
|
||||
accessKey: "C",
|
||||
accessKey: GetStringFromName("changeAutofillOptionsAccessKey"),
|
||||
callbackState: "open-pref",
|
||||
disableHighlight: true,
|
||||
},
|
||||
@ -82,12 +82,12 @@ const CONTENT = {
|
||||
},
|
||||
mainAction: {
|
||||
label: GetStringFromName("updateAddressLabel"),
|
||||
accessKey: "U",
|
||||
accessKey: GetStringFromName("updateAddressAccessKey"),
|
||||
callbackState: "update",
|
||||
},
|
||||
secondaryActions: [{
|
||||
label: GetStringFromName("createAddressLabel"),
|
||||
accessKey: "C",
|
||||
accessKey: GetStringFromName("createAddressAccessKey"),
|
||||
callbackState: "create",
|
||||
}],
|
||||
options: {
|
||||
@ -107,16 +107,16 @@ const CONTENT = {
|
||||
},
|
||||
mainAction: {
|
||||
label: GetStringFromName("saveCreditCardLabel"),
|
||||
accessKey: "S",
|
||||
accessKey: GetStringFromName("saveCreditCardAccessKey"),
|
||||
callbackState: "save",
|
||||
},
|
||||
secondaryActions: [{
|
||||
label: GetStringFromName("cancelCreditCardLabel"),
|
||||
accessKey: "D",
|
||||
accessKey: GetStringFromName("cancelCreditCardAccessKey"),
|
||||
callbackState: "cancel",
|
||||
}, {
|
||||
label: GetStringFromName("neverSaveCreditCardLabel"),
|
||||
accessKey: "N",
|
||||
accessKey: GetStringFromName("neverSaveCreditCardAccessKey"),
|
||||
callbackState: "disable",
|
||||
}],
|
||||
options: {
|
||||
@ -160,12 +160,12 @@ const CONTENT = {
|
||||
},
|
||||
mainAction: {
|
||||
label: GetStringFromName("updateCreditCardLabel"),
|
||||
accessKey: "U",
|
||||
accessKey: GetStringFromName("updateCreditCardAccessKey"),
|
||||
callbackState: "update",
|
||||
},
|
||||
secondaryActions: [{
|
||||
label: GetStringFromName("createCreditCardLabel"),
|
||||
accessKey: "C",
|
||||
accessKey: GetStringFromName("createCreditCardAccessKey"),
|
||||
callbackState: "create",
|
||||
}],
|
||||
options: {
|
||||
|
@ -17,6 +17,7 @@ autofillSecurityOptionsLinkOSX = Form Autofill & Security Preferences
|
||||
# that notifies users that addresses are saved. The button leads users to Form Autofill browser preferences.
|
||||
changeAutofillOptions = Change Form Autofill Options
|
||||
changeAutofillOptionsOSX = Change Form Autofill Preferences
|
||||
changeAutofillOptionsAccessKey = C
|
||||
# LOCALIZATION NOTE (addressesSyncCheckbox): If Sync is enabled, this checkbox is displayed on the doorhanger
|
||||
# shown when saving addresses.
|
||||
addressesSyncCheckbox = Share addresses with synced devices
|
||||
@ -27,19 +28,26 @@ creditCardsSyncCheckbox = Share credit cards with synced devices
|
||||
# when an address change is detected.
|
||||
updateAddressMessage = Would you like to update your address with this new information?
|
||||
createAddressLabel = Create New Address
|
||||
createAddressAccessKey = C
|
||||
updateAddressLabel = Update Address
|
||||
updateAddressAccessKey = U
|
||||
# LOCALIZATION NOTE (saveCreditCardMessage, saveCreditCardLabel, cancelCreditCardLabel, neverSaveCreditCardLabel):
|
||||
# Used on the doorhanger when users submit payment with credit card.
|
||||
# LOCALIZATION NOTE (saveCreditCardMessage): %S is brandShortName.
|
||||
saveCreditCardMessage = Would you like %S to save this credit card? (Security code will not be saved)
|
||||
saveCreditCardLabel = Save Credit Card
|
||||
saveCreditCardAccessKey = S
|
||||
cancelCreditCardLabel = Don’t Save
|
||||
cancelCreditCardAccessKey = D
|
||||
neverSaveCreditCardLabel = Never Save Credit Cards
|
||||
neverSaveCreditCardAccessKey = N
|
||||
# LOCALIZATION NOTE (updateCreditCardMessage, createCreditCardLabel, updateCreditCardLabel): Used on the doorhanger
|
||||
# when an credit card change is detected.
|
||||
updateCreditCardMessage = Would you like to update your credit card with this new information?
|
||||
createCreditCardLabel = Create New Credit Card
|
||||
createCreditCardAccessKey = C
|
||||
updateCreditCardLabel = Update Credit Card
|
||||
updateCreditCardAccessKey = U
|
||||
# LOCALIZATION NOTE (openAutofillMessagePanel): Tooltip label for Form Autofill doorhanger icon on address bar.
|
||||
openAutofillMessagePanel = Open Form Autofill message panel
|
||||
|
||||
|
2
browser/extensions/pocket/bootstrap.js
vendored
2
browser/extensions/pocket/bootstrap.js
vendored
@ -94,7 +94,7 @@ var PocketPageAction = {
|
||||
this.pageAction = PageActions.addAction(new PageActions.Action({
|
||||
id,
|
||||
title: gPocketBundle.GetStringFromName("saveToPocketCmd.label"),
|
||||
shownInUrlbar: true,
|
||||
pinnedToUrlbar: true,
|
||||
wantsIframe: true,
|
||||
urlbarIDOverride: "pocket-button-box",
|
||||
anchorIDOverride: "pocket-button",
|
||||
|
@ -46,6 +46,7 @@
|
||||
|
||||
[@AB_CD@]
|
||||
@RESPATH@/dictionaries/*
|
||||
@RESPATH@/browser/localization/*
|
||||
#if defined(XP_WIN) || defined(XP_LINUX)
|
||||
@RESPATH@/fonts/*
|
||||
#endif
|
||||
|
@ -96,6 +96,7 @@ uk
|
||||
ur
|
||||
uz
|
||||
vi
|
||||
wo
|
||||
xh
|
||||
zh-CN
|
||||
zh-TW
|
||||
|
4
browser/locales/en-US/browser/preferences/main.ftl
Normal file
4
browser/locales/en-US/browser/preferences/main.ftl
Normal file
@ -0,0 +1,4 @@
|
||||
// Variables:
|
||||
// $num - default value of the `dom.ipc.processCount` pref.
|
||||
default-content-process-count
|
||||
.label = { $num } (default)
|
@ -961,6 +961,9 @@ you can use these alternative items. Otherwise, their values should be empty. -
|
||||
<!ENTITY pageActionButton.tooltip "Page actions">
|
||||
<!ENTITY pageAction.addToUrlbar.label "Add to Address Bar">
|
||||
<!ENTITY pageAction.removeFromUrlbar.label "Remove from Address Bar">
|
||||
<!ENTITY pageAction.allowInUrlbar.label "Show in Address Bar">
|
||||
<!ENTITY pageAction.disallowInUrlbar.label "Don’t Show in Address Bar">
|
||||
<!ENTITY pageAction.manageExtension.label "Manage Extension…">
|
||||
|
||||
<!ENTITY pageAction.sendTabToDevice.label "Send Tab to Device">
|
||||
<!ENTITY sendToDevice.syncNotReady.label "Syncing Devices…">
|
||||
|
@ -55,6 +55,7 @@
|
||||
<!ENTITY permAskAlways "Always ask">
|
||||
<!ENTITY permAllow "Allow">
|
||||
<!ENTITY permAllowSession "Allow for Session">
|
||||
<!ENTITY permHide "Hide">
|
||||
<!ENTITY permBlock "Block">
|
||||
<!ENTITY permissionsFor "Permissions for:">
|
||||
<!ENTITY permPlugins "Activate Plugins">
|
||||
|
@ -7,6 +7,9 @@
|
||||
# override and resource entries should go to browser/base/jar.mn to avoid
|
||||
# having to create the same entry for each locale.
|
||||
|
||||
[localization] @AB_CD@.jar:
|
||||
browser (%browser/**/*.ftl)
|
||||
|
||||
@AB_CD@.jar:
|
||||
% locale browser @AB_CD@ %locale/browser/
|
||||
* locale/browser/bookmarks.html (generic/profile/bookmarks.html.in)
|
||||
|
@ -103,6 +103,7 @@ locales = [
|
||||
"ur",
|
||||
"uz",
|
||||
"vi",
|
||||
"wo",
|
||||
"xh",
|
||||
"zh-CN",
|
||||
"zh-TW",
|
||||
|
@ -119,15 +119,20 @@ this.PageActions = {
|
||||
},
|
||||
|
||||
/**
|
||||
* The list of actions in the urlbar, sorted in the order in which they should
|
||||
* appear there. Not live. (array of Action objects)
|
||||
* The list of actions currently in the urlbar, sorted in the order in which
|
||||
* they appear. Not live.
|
||||
*
|
||||
* @param browserWindow (DOM window, required)
|
||||
* This window's actions will be returned.
|
||||
* @return (array of PageAction.Action objects) The actions currently in the
|
||||
* given window's urlbar.
|
||||
*/
|
||||
get actionsInUrlbar() {
|
||||
actionsInUrlbar(browserWindow) {
|
||||
// Remember that IDs in idsInUrlbar may belong to actions that aren't
|
||||
// currently registered.
|
||||
return this._persistedActions.idsInUrlbar.reduce((actions, id) => {
|
||||
let action = this.actionForID(id);
|
||||
if (action) {
|
||||
if (action && action.shouldShowInUrlbar(browserWindow)) {
|
||||
actions.push(action);
|
||||
}
|
||||
return actions;
|
||||
@ -228,30 +233,29 @@ this.PageActions = {
|
||||
}
|
||||
|
||||
if (this._persistedActions.ids.includes(action.id)) {
|
||||
// The action has been seen before. Override its shownInUrlbar value
|
||||
// The action has been seen before. Override its pinnedToUrlbar value
|
||||
// with the persisted value. Set the private version of that property
|
||||
// so that onActionToggledShownInUrlbar isn't called, which happens when
|
||||
// so that onActionToggledPinnedToUrlbar isn't called, which happens when
|
||||
// the public version is set.
|
||||
action._shownInUrlbar =
|
||||
action._pinnedToUrlbar =
|
||||
this._persistedActions.idsInUrlbar.includes(action.id);
|
||||
} else {
|
||||
// The action is new. Store it in the persisted actions.
|
||||
this._persistedActions.ids.push(action.id);
|
||||
this._updateIDsInUrlbarForAction(action);
|
||||
this._updateIDsPinnedToUrlbarForAction(action);
|
||||
}
|
||||
},
|
||||
|
||||
_updateIDsInUrlbarForAction(action) {
|
||||
_updateIDsPinnedToUrlbarForAction(action) {
|
||||
let index = this._persistedActions.idsInUrlbar.indexOf(action.id);
|
||||
if (action.shownInUrlbar) {
|
||||
if (action.pinnedToUrlbar) {
|
||||
if (index < 0) {
|
||||
let nextID = this.nextActionIDInUrlbar(action.id);
|
||||
let nextIndex =
|
||||
nextID ? this._persistedActions.idsInUrlbar.indexOf(nextID) : -1;
|
||||
if (nextIndex < 0) {
|
||||
nextIndex = this._persistedActions.idsInUrlbar.length;
|
||||
index = action.id == ACTION_ID_BOOKMARK ? -1 :
|
||||
this._persistedActions.idsInUrlbar.indexOf(ACTION_ID_BOOKMARK);
|
||||
if (index < 0) {
|
||||
index = this._persistedActions.idsInUrlbar.length;
|
||||
}
|
||||
this._persistedActions.idsInUrlbar.splice(nextIndex, 0, action.id);
|
||||
this._persistedActions.idsInUrlbar.splice(index, 0, action.id);
|
||||
}
|
||||
} else if (index >= 0) {
|
||||
this._persistedActions.idsInUrlbar.splice(index, 1);
|
||||
@ -273,13 +277,13 @@ this.PageActions = {
|
||||
* @return The ID of the reference action, or null if your action should be
|
||||
* appended.
|
||||
*/
|
||||
nextActionIDInUrlbar(action) {
|
||||
nextActionIDInUrlbar(browserWindow, action) {
|
||||
// Actions in the urlbar are always inserted before the bookmark action,
|
||||
// which always comes last if it's present.
|
||||
if (action.id == ACTION_ID_BOOKMARK) {
|
||||
return null;
|
||||
}
|
||||
let id = this._nextActionID(action, this.actionsInUrlbar);
|
||||
let id = this._nextActionID(action, this.actionsInUrlbar(browserWindow));
|
||||
return id || ACTION_ID_BOOKMARK;
|
||||
},
|
||||
|
||||
@ -354,34 +358,32 @@ this.PageActions = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Call this when an action's shownInUrlbar property changes.
|
||||
* Call this when an action's pinnedToUrlbar property changes.
|
||||
*
|
||||
* @param action (Action object, required)
|
||||
* The action whose shownInUrlbar property changed.
|
||||
* The action whose pinnedToUrlbar property changed.
|
||||
*/
|
||||
onActionToggledShownInUrlbar(action) {
|
||||
onActionToggledPinnedToUrlbar(action) {
|
||||
if (!this.actionForID(action.id)) {
|
||||
// This may be called before the action has been added.
|
||||
return;
|
||||
}
|
||||
this._updateIDsInUrlbarForAction(action);
|
||||
this._updateIDsPinnedToUrlbarForAction(action);
|
||||
for (let bpa of allBrowserPageActions()) {
|
||||
bpa.placeActionInUrlbar(action);
|
||||
}
|
||||
},
|
||||
|
||||
logTelemetry(type, action, node = null) {
|
||||
const kAllowedLabels = ["pocket", "screenshots", "webcompat"].concat(
|
||||
gBuiltInActions.filter(a => !a.__isSeparator).map(a => a.id)
|
||||
);
|
||||
|
||||
if (type == "used") {
|
||||
type = (node && node.closest("#urlbar-container")) ? "urlbar_used" : "panel_used";
|
||||
type =
|
||||
node && node.closest("#urlbar-container") ? "urlbar_used" :
|
||||
"panel_used";
|
||||
}
|
||||
let histogramID = "FX_PAGE_ACTION_" + type.toUpperCase();
|
||||
try {
|
||||
let histogram = Services.telemetry.getHistogramById(histogramID);
|
||||
if (kAllowedLabels.includes(action.labelForHistogram)) {
|
||||
if (action._isBuiltIn) {
|
||||
histogram.add(action.labelForHistogram);
|
||||
} else {
|
||||
histogram.add("other");
|
||||
@ -492,6 +494,8 @@ this.PageActions = {
|
||||
* @param disabled (bool, optional)
|
||||
* Pass true to cause the action to be disabled initially in all browser
|
||||
* windows. False by default.
|
||||
* @param extensionID (string, optional)
|
||||
* If the action lives in an extension, pass its ID.
|
||||
* @param iconURL (string or object, optional)
|
||||
* The URL string of the action's icon. Usually you want to specify an
|
||||
* icon in CSS, but this option is useful if that would be a pain for
|
||||
@ -549,9 +553,9 @@ this.PageActions = {
|
||||
* Called when a browser window's page action panel is showing:
|
||||
* onShowingInPanel(buttonNode)
|
||||
* * buttonNode: The action's node in the page action panel.
|
||||
* @param shownInUrlbar (bool, optional)
|
||||
* Pass true to show the action in the urlbar, false otherwise. False by
|
||||
* default.
|
||||
* @param pinnedToUrlbar (bool, optional)
|
||||
* Pass true to pin the action to the urlbar. An action is shown in the
|
||||
* urlbar if it's pinned and not disabled. False by default.
|
||||
* @param subview (object, optional)
|
||||
* An options object suitable for passing to the Subview constructor, if
|
||||
* you'd like the action to have a subview. See the subview constructor
|
||||
@ -572,6 +576,7 @@ function Action(options) {
|
||||
title: !options._isSeparator,
|
||||
anchorIDOverride: false,
|
||||
disabled: false,
|
||||
extensionID: false,
|
||||
iconURL: false,
|
||||
labelForHistogram: false,
|
||||
nodeAttributes: false,
|
||||
@ -585,7 +590,7 @@ function Action(options) {
|
||||
onPlacedInUrlbar: false,
|
||||
onRemovedFromWindow: false,
|
||||
onShowingInPanel: false,
|
||||
shownInUrlbar: false,
|
||||
pinnedToUrlbar: false,
|
||||
subview: false,
|
||||
tooltip: false,
|
||||
urlbarIDOverride: false,
|
||||
@ -617,6 +622,13 @@ function Action(options) {
|
||||
}
|
||||
|
||||
Action.prototype = {
|
||||
/**
|
||||
* The ID of the action's parent extension (string, nullable)
|
||||
*/
|
||||
get extensionID() {
|
||||
return this._extensionID;
|
||||
},
|
||||
|
||||
/**
|
||||
* The action's ID (string, nonnull)
|
||||
*/
|
||||
@ -633,17 +645,18 @@ Action.prototype = {
|
||||
},
|
||||
|
||||
/**
|
||||
* True if the action is shown in the urlbar (bool, nonnull)
|
||||
* True if the action is pinned to the urlbar. The action is shown in the
|
||||
* urlbar if it's pinned and not disabled. (bool, nonnull)
|
||||
*/
|
||||
get shownInUrlbar() {
|
||||
return this._shownInUrlbar || false;
|
||||
get pinnedToUrlbar() {
|
||||
return this._pinnedToUrlbar || false;
|
||||
},
|
||||
set shownInUrlbar(shown) {
|
||||
if (this.shownInUrlbar != shown) {
|
||||
this._shownInUrlbar = shown;
|
||||
PageActions.onActionToggledShownInUrlbar(this);
|
||||
set pinnedToUrlbar(shown) {
|
||||
if (this.pinnedToUrlbar != shown) {
|
||||
this._pinnedToUrlbar = shown;
|
||||
PageActions.onActionToggledPinnedToUrlbar(this);
|
||||
}
|
||||
return this.shownInUrlbar;
|
||||
return this.pinnedToUrlbar;
|
||||
},
|
||||
|
||||
/**
|
||||
@ -972,7 +985,28 @@ Action.prototype = {
|
||||
*/
|
||||
remove() {
|
||||
PageActions.onActionRemoved(this);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns whether the action should be shown in a given window's urlbar.
|
||||
*
|
||||
* @param browserWindow (DOM window, required)
|
||||
* The window.
|
||||
* @return True if the action should be shown and false otherwise. The action
|
||||
* should be shown if it's both pinned and not disabled.
|
||||
*/
|
||||
shouldShowInUrlbar(browserWindow) {
|
||||
return this.pinnedToUrlbar && !this.getDisabled(browserWindow);
|
||||
},
|
||||
|
||||
get _isBuiltIn() {
|
||||
let builtInIDs = [
|
||||
"pocket",
|
||||
"screenshots",
|
||||
"webcompat-reporter-button",
|
||||
].concat(gBuiltInActions.filter(a => !a.__isSeparator).map(a => a.id));
|
||||
return builtInIDs.includes(this.id);
|
||||
},
|
||||
};
|
||||
|
||||
this.PageActions.Action = Action;
|
||||
@ -1146,7 +1180,7 @@ var gBuiltInActions = [
|
||||
// The title is set in browser-pageActions.js by calling
|
||||
// BookmarkingUI.updateBookmarkPageMenuItem().
|
||||
title: "",
|
||||
shownInUrlbar: true,
|
||||
pinnedToUrlbar: true,
|
||||
nodeAttributes: {
|
||||
observes: "bookmarkThisPageBroadcaster",
|
||||
},
|
||||
|
@ -30,9 +30,14 @@ this.PluginContent = function(global) {
|
||||
const FLASH_MIME_TYPE = "application/x-shockwave-flash";
|
||||
const REPLACEMENT_STYLE_SHEET = Services.io.newURI("chrome://pluginproblem/content/pluginReplaceBinding.css");
|
||||
|
||||
const OVERLAY_DISPLAY_HIDDEN = 0;
|
||||
const OVERLAY_DISPLAY_VISIBLE = 1;
|
||||
const OVERLAY_DISPLAY_MINIMAL = 2;
|
||||
const OVERLAY_DISPLAY = {
|
||||
HIDDEN: 0, // The overlay will be transparent
|
||||
BLANK: 1, // The overlay will be just a grey box
|
||||
TINY: 2, // The overlay with a 16x16 plugin icon
|
||||
REDUCED: 3, // The overlay with a 32x32 plugin icon
|
||||
NOTEXT: 4, // The overlay with a 48x48 plugin icon and the close button
|
||||
FULL: 5, // The full overlay: 48x48 plugin icon, close button and label
|
||||
};
|
||||
|
||||
PluginContent.prototype = {
|
||||
init(global) {
|
||||
@ -289,44 +294,80 @@ PluginContent.prototype = {
|
||||
* Update the visibility of the plugin overlay.
|
||||
*/
|
||||
setVisibility(plugin, overlay, overlayDisplayState) {
|
||||
overlay.classList.toggle("visible", overlayDisplayState != OVERLAY_DISPLAY_HIDDEN);
|
||||
overlay.classList.toggle("minimal", overlayDisplayState == OVERLAY_DISPLAY_MINIMAL)
|
||||
if (overlayDisplayState == OVERLAY_DISPLAY_VISIBLE) {
|
||||
overlay.classList.toggle("visible", overlayDisplayState != OVERLAY_DISPLAY.HIDDEN);
|
||||
if (overlayDisplayState != OVERLAY_DISPLAY.HIDDEN) {
|
||||
overlay.removeAttribute("dismissed");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check whether the plugin should be visible on the page. A plugin should
|
||||
* not be visible if the overlay is too big, or if any other page content
|
||||
* overlays it.
|
||||
* Adjust the style in which the overlay will be displayed. It might be adjusted
|
||||
* based on its size, or if there's some other element covering all corners of
|
||||
* the overlay.
|
||||
*
|
||||
* This function will handle showing or hiding the overlay.
|
||||
* @returns true if the plugin is invisible.
|
||||
* This function will handle adjusting the style of the overlay, but will
|
||||
* not handle hiding it. That is done by setVisibility with the return value
|
||||
* from this function.
|
||||
*
|
||||
* @returns A value from OVERLAY_DISPLAY.
|
||||
*/
|
||||
computeOverlayDisplayState(plugin, overlay) {
|
||||
computeAndAdjustOverlayDisplay(plugin, overlay) {
|
||||
let fallbackType = plugin.pluginFallbackType;
|
||||
if (plugin.pluginFallbackTypeOverride !== undefined) {
|
||||
fallbackType = plugin.pluginFallbackTypeOverride;
|
||||
}
|
||||
if (fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY_QUIET) {
|
||||
return OVERLAY_DISPLAY_HIDDEN;
|
||||
return OVERLAY_DISPLAY.HIDDEN;
|
||||
}
|
||||
|
||||
// If the overlay size is 0, we haven't done layout yet. Presume that
|
||||
// plugins are visible until we know otherwise.
|
||||
if (overlay.scrollWidth == 0) {
|
||||
return OVERLAY_DISPLAY_VISIBLE;
|
||||
return OVERLAY_DISPLAY.FULL;
|
||||
}
|
||||
|
||||
let overlayDisplay = OVERLAY_DISPLAY.FULL;
|
||||
|
||||
// Is the <object>'s size too small to hold what we want to show?
|
||||
let pluginRect = plugin.getBoundingClientRect();
|
||||
let pluginWidth = Math.ceil(pluginRect.width);
|
||||
let pluginHeight = Math.ceil(pluginRect.height);
|
||||
|
||||
// We must set the attributes while here inside this function in order
|
||||
// for a possible re-style to occur, which will make the scrollWidth/Height
|
||||
// checks below correct. Otherwise, we would be requesting e.g. a TINY
|
||||
// overlay here, but the default styling would be used, and that would make
|
||||
// it overflow, causing it to change to BLANK instead of remaining as TINY.
|
||||
|
||||
if (pluginWidth <= 32 || pluginHeight <= 32) {
|
||||
overlay.setAttribute("sizing", "blank");
|
||||
overlayDisplay = OVERLAY_DISPLAY.BLANK;
|
||||
} else if (pluginWidth <= 80 || pluginHeight <= 60) {
|
||||
overlayDisplay = OVERLAY_DISPLAY.TINY;
|
||||
overlay.setAttribute("sizing", "tiny");
|
||||
overlay.setAttribute("notext", "notext");
|
||||
} else if (pluginWidth <= 120 || pluginHeight <= 80) {
|
||||
overlayDisplay = OVERLAY_DISPLAY.REDUCED;
|
||||
overlay.setAttribute("sizing", "reduced");
|
||||
overlay.setAttribute("notext", "notext");
|
||||
} else if (pluginWidth <= 240 || pluginHeight <= 160) {
|
||||
overlayDisplay = OVERLAY_DISPLAY.NOTEXT;
|
||||
overlay.removeAttribute("sizing");
|
||||
overlay.setAttribute("notext", "notext");
|
||||
} else {
|
||||
overlayDisplay = OVERLAY_DISPLAY.FULL;
|
||||
overlay.removeAttribute("sizing");
|
||||
overlay.removeAttribute("notext");
|
||||
}
|
||||
|
||||
|
||||
// XXX bug 446693. The text-shadow on the submitted-report text at
|
||||
// the bottom causes scrollHeight to be larger than it should be.
|
||||
let overflows = (overlay.scrollWidth > Math.ceil(pluginRect.width)) ||
|
||||
(overlay.scrollHeight - 5 > Math.ceil(pluginRect.height));
|
||||
let overflows = (overlay.scrollWidth > pluginWidth) ||
|
||||
(overlay.scrollHeight - 5 > pluginHeight);
|
||||
if (overflows) {
|
||||
return OVERLAY_DISPLAY_MINIMAL;
|
||||
overlay.setAttribute("sizing", "blank");
|
||||
return OVERLAY_DISPLAY.BLANK;
|
||||
}
|
||||
|
||||
// Is the plugin covered up by other content so that it is not clickable?
|
||||
@ -353,11 +394,12 @@ PluginContent.prototype = {
|
||||
}
|
||||
let el = cwu.elementFromPoint(x, y, true, true);
|
||||
if (el === plugin) {
|
||||
return OVERLAY_DISPLAY_VISIBLE;
|
||||
return overlayDisplay;
|
||||
}
|
||||
}
|
||||
|
||||
return OVERLAY_DISPLAY_HIDDEN;
|
||||
overlay.setAttribute("sizing", "blank");
|
||||
return OVERLAY_DISPLAY.BLANK;
|
||||
},
|
||||
|
||||
addLinkClickCallback(linkNode, callbackName /* callbackArgs...*/) {
|
||||
@ -476,7 +518,7 @@ PluginContent.prototype = {
|
||||
if (eventType == "PluginPlaceholderReplaced") {
|
||||
plugin.removeAttribute("href");
|
||||
let overlay = this.getPluginUI(plugin, "main");
|
||||
this.setVisibility(plugin, overlay, OVERLAY_DISPLAY_VISIBLE);
|
||||
this.setVisibility(plugin, overlay, OVERLAY_DISPLAY.FULL);
|
||||
let inIDOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"]
|
||||
.getService(Ci.inIDOMUtils);
|
||||
// Add psuedo class so our styling will take effect
|
||||
@ -561,10 +603,10 @@ PluginContent.prototype = {
|
||||
if (eventType != "PluginCrashed") {
|
||||
if (overlay != null) {
|
||||
this.setVisibility(plugin, overlay,
|
||||
this.computeOverlayDisplayState(plugin, overlay));
|
||||
this.computeAndAdjustOverlayDisplay(plugin, overlay));
|
||||
let resizeListener = () => {
|
||||
this.setVisibility(plugin, overlay,
|
||||
this.computeOverlayDisplayState(plugin, overlay));
|
||||
this.computeAndAdjustOverlayDisplay(plugin, overlay));
|
||||
this.updateNotificationUI();
|
||||
};
|
||||
plugin.addEventListener("overflow", resizeListener);
|
||||
@ -903,9 +945,9 @@ PluginContent.prototype = {
|
||||
if (!overlay) {
|
||||
continue;
|
||||
}
|
||||
let overlayDisplayState = this.computeOverlayDisplayState(plugin, overlay);
|
||||
let overlayDisplayState = this.computeAndAdjustOverlayDisplay(plugin, overlay);
|
||||
this.setVisibility(plugin, overlay, overlayDisplayState);
|
||||
if (overlayDisplayState == OVERLAY_DISPLAY_VISIBLE) {
|
||||
if (overlayDisplayState > OVERLAY_DISPLAY.BLANK) {
|
||||
actions.delete(info.permissionString);
|
||||
if (actions.size == 0) {
|
||||
break;
|
||||
@ -1089,21 +1131,21 @@ PluginContent.prototype = {
|
||||
let link = this.getPluginUI(plugin, "reloadLink");
|
||||
this.addLinkClickCallback(link, "reloadPage");
|
||||
|
||||
let overlayDisplayState = this.computeOverlayDisplayState(plugin, overlay);
|
||||
let overlayDisplayState = this.computeAndAdjustOverlayDisplay(plugin, overlay);
|
||||
|
||||
// Is the <object>'s size too small to hold what we want to show?
|
||||
if (overlayDisplayState != OVERLAY_DISPLAY_VISIBLE) {
|
||||
if (overlayDisplayState != OVERLAY_DISPLAY.FULL) {
|
||||
// First try hiding the crash report submission UI.
|
||||
statusDiv.removeAttribute("status");
|
||||
|
||||
overlayDisplayState = this.computeOverlayDisplayState(plugin, overlay);
|
||||
overlayDisplayState = this.computeAndAdjustOverlayDisplay(plugin, overlay);
|
||||
}
|
||||
this.setVisibility(plugin, overlay, overlayDisplayState);
|
||||
|
||||
let doc = plugin.ownerDocument;
|
||||
let runID = plugin.runID;
|
||||
|
||||
if (overlayDisplayState == OVERLAY_DISPLAY_VISIBLE) {
|
||||
if (overlayDisplayState == OVERLAY_DISPLAY.FULL) {
|
||||
// If a previous plugin on the page was too small and resulted in adding a
|
||||
// notification bar, then remove it because this plugin instance it big
|
||||
// enough to serve as in-content notification.
|
||||
|
@ -1,4 +1,3 @@
|
||||
/* eslint-disable mozilla/no-arbitrary-setTimeout */
|
||||
"use strict";
|
||||
|
||||
// This is a test for PageActions.jsm, specifically the generalized parts that
|
||||
@ -77,10 +76,14 @@ add_task(async function simple() {
|
||||
Assert.equal(action.getIconURL(), iconURL, "iconURL");
|
||||
Assert.equal(action.id, id, "id");
|
||||
Assert.deepEqual(action.nodeAttributes, nodeAttributes, "nodeAttributes");
|
||||
Assert.equal(action.shownInUrlbar, false, "shownInUrlbar");
|
||||
Assert.equal(action.pinnedToUrlbar, false, "pinnedToUrlbar");
|
||||
Assert.equal(action.subview, null, "subview");
|
||||
Assert.equal(action.getDisabled(), false, "disabled");
|
||||
Assert.equal(action.getDisabled(window), false, "disabled in window");
|
||||
Assert.equal(action.getTitle(), title, "title");
|
||||
Assert.equal(action.getTitle(window), title, "title in window");
|
||||
Assert.equal(action.getTooltip(), tooltip, "tooltip");
|
||||
Assert.equal(action.getTooltip(window), tooltip, "tooltip in window");
|
||||
Assert.equal(action.urlbarIDOverride, null, "urlbarIDOverride");
|
||||
Assert.equal(action.wantsIframe, false, "wantsIframe");
|
||||
|
||||
@ -151,7 +154,7 @@ add_task(async function simple() {
|
||||
Assert.equal(onCommandCallCount, 1, "onCommandCallCount should be inc'ed");
|
||||
|
||||
// Show the action's button in the urlbar.
|
||||
action.shownInUrlbar = true;
|
||||
action.pinnedToUrlbar = true;
|
||||
Assert.equal(onPlacedInUrlbarCallCount, 1,
|
||||
"onPlacedInUrlbarCallCount should be inc'ed");
|
||||
urlbarButtonNode = document.getElementById(urlbarButtonID);
|
||||
@ -172,6 +175,22 @@ add_task(async function simple() {
|
||||
"Next node should be the bookmark star"
|
||||
);
|
||||
|
||||
// Disable the action. The button in the urlbar should be removed, and the
|
||||
// button in the panel should be disabled.
|
||||
action.setDisabled(true);
|
||||
urlbarButtonNode = document.getElementById(urlbarButtonID);
|
||||
Assert.equal(urlbarButtonNode, null, "urlbar button should be removed");
|
||||
Assert.equal(panelButtonNode.disabled, true,
|
||||
"panel button should be disabled");
|
||||
|
||||
// Enable the action. The button in the urlbar should be added back, and the
|
||||
// button in the panel should be enabled.
|
||||
action.setDisabled(false);
|
||||
urlbarButtonNode = document.getElementById(urlbarButtonID);
|
||||
Assert.notEqual(urlbarButtonNode, null, "urlbar button should be added back");
|
||||
Assert.equal(panelButtonNode.disabled, false,
|
||||
"panel button should not be disabled");
|
||||
|
||||
// Click the urlbar button.
|
||||
onCommandExpectedButtonID = urlbarButtonID;
|
||||
EventUtils.synthesizeMouseAtCenter(urlbarButtonNode, {});
|
||||
@ -184,7 +203,7 @@ add_task(async function simple() {
|
||||
Assert.equal(panelButtonNode.getAttribute("label"), action.getTitle(),
|
||||
"New label");
|
||||
|
||||
// Now that shownInUrlbar has been toggled, make sure that it sticks across
|
||||
// Now that pinnedToUrlbar has been toggled, make sure that it sticks across
|
||||
// app restarts. Simulate that by "unregistering" the action (not by removing
|
||||
// it, which is more permanent) and then registering it again.
|
||||
|
||||
@ -202,10 +221,10 @@ add_task(async function simple() {
|
||||
"PageActions should have 'seen' the action");
|
||||
Assert.ok(PageActions._persistedActions.idsInUrlbar.includes(action.id),
|
||||
"idsInUrlbar should still include the action");
|
||||
Assert.ok(action.shownInUrlbar,
|
||||
"shownInUrlbar should still be true");
|
||||
Assert.ok(action._shownInUrlbar,
|
||||
"_shownInUrlbar should still be true, for good measure");
|
||||
Assert.ok(action.pinnedToUrlbar,
|
||||
"pinnedToUrlbar should still be true");
|
||||
Assert.ok(action._pinnedToUrlbar,
|
||||
"_pinnedToUrlbar should still be true, for good measure");
|
||||
|
||||
// Remove the action.
|
||||
action.remove();
|
||||
@ -301,7 +320,7 @@ add_task(async function withSubview() {
|
||||
let action = PageActions.addAction(new PageActions.Action({
|
||||
iconURL: "chrome://browser/skin/mail.svg",
|
||||
id,
|
||||
shownInUrlbar: true,
|
||||
pinnedToUrlbar: true,
|
||||
subview,
|
||||
title: "Test subview",
|
||||
onCommand(event, buttonNode) {
|
||||
@ -444,7 +463,7 @@ add_task(async function withIframe() {
|
||||
let action = PageActions.addAction(new PageActions.Action({
|
||||
iconURL: "chrome://browser/skin/mail.svg",
|
||||
id,
|
||||
shownInUrlbar: true,
|
||||
pinnedToUrlbar: true,
|
||||
title: "Test iframe",
|
||||
wantsIframe: true,
|
||||
onCommand(event, buttonNode) {
|
||||
@ -530,7 +549,7 @@ add_task(async function withIframe() {
|
||||
await promisePanelHidden(BrowserPageActions._activatedActionPanelID);
|
||||
|
||||
// Hide the action's button in the urlbar.
|
||||
action.shownInUrlbar = false;
|
||||
action.pinnedToUrlbar = false;
|
||||
urlbarButtonNode = document.getElementById(urlbarButtonID);
|
||||
Assert.equal(urlbarButtonNode, null, "urlbarButtonNode");
|
||||
|
||||
@ -830,7 +849,7 @@ add_task(async function urlbarOrderNewWindow() {
|
||||
return PageActions.addAction(new PageActions.Action({
|
||||
id: `test-urlbarOrderNewWindow-${i}`,
|
||||
title: `Test urlbarOrderNewWindow ${i}`,
|
||||
shownInUrlbar: true,
|
||||
pinnedToUrlbar: true,
|
||||
}));
|
||||
});
|
||||
|
||||
@ -844,8 +863,8 @@ add_task(async function urlbarOrderNewWindow() {
|
||||
"PageActions._persistedActions.idsInUrlbar has new actions inserted"
|
||||
);
|
||||
Assert.deepEqual(
|
||||
PageActions.actionsInUrlbar.slice(
|
||||
PageActions.actionsInUrlbar.length - (actions.length + 1)
|
||||
PageActions.actionsInUrlbar(window).slice(
|
||||
PageActions.actionsInUrlbar(window).length - (actions.length + 1)
|
||||
).map(a => a.id),
|
||||
actions.map(a => a.id).concat([PageActions.ACTION_ID_BOOKMARK]),
|
||||
"PageActions.actionsInUrlbar has new actions inserted"
|
||||
@ -934,9 +953,9 @@ add_task(async function migrate1() {
|
||||
|
||||
Assert.equal(PageActions._persistedActions.version, 1, "Correct version");
|
||||
|
||||
// Need to set copyURL's _shownInUrlbar. It won't be set since it's false by
|
||||
// Need to set copyURL's _pinnedToUrlbar. It won't be set since it's false by
|
||||
// default and we reached directly into persisted storage above.
|
||||
PageActions.actionForID("copyURL")._shownInUrlbar = true;
|
||||
PageActions.actionForID("copyURL")._pinnedToUrlbar = true;
|
||||
|
||||
// expected order
|
||||
let orderedIDs = [
|
||||
@ -952,7 +971,7 @@ add_task(async function migrate1() {
|
||||
"PageActions._persistedActions.idsInUrlbar has right order"
|
||||
);
|
||||
Assert.deepEqual(
|
||||
PageActions.actionsInUrlbar.map(a => a.id),
|
||||
PageActions.actionsInUrlbar(window).map(a => a.id),
|
||||
orderedIDs,
|
||||
"PageActions.actionsInUrlbar has right order"
|
||||
);
|
||||
@ -982,7 +1001,7 @@ add_task(async function migrate1() {
|
||||
// Done, clean up.
|
||||
await BrowserTestUtils.closeWindow(win);
|
||||
Services.prefs.clearUserPref(PageActions.PREF_PERSISTED_ACTIONS);
|
||||
PageActions.actionForID("copyURL").shownInUrlbar = false;
|
||||
PageActions.actionForID("copyURL").pinnedToUrlbar = false;
|
||||
});
|
||||
|
||||
|
||||
@ -993,10 +1012,12 @@ add_task(async function perWindowState() {
|
||||
let action = PageActions.addAction(new PageActions.Action({
|
||||
iconURL: "chrome://browser/skin/mail.svg",
|
||||
id: "test-perWindowState",
|
||||
shownInUrlbar: true,
|
||||
pinnedToUrlbar: true,
|
||||
title,
|
||||
}));
|
||||
|
||||
let actionsInUrlbar = PageActions.actionsInUrlbar(window);
|
||||
|
||||
// Open a new browser window and load an actionable page so that the action
|
||||
// shows up in it.
|
||||
let newWindow = await BrowserTestUtils.openNewBrowserWindow();
|
||||
@ -1043,6 +1064,53 @@ add_task(async function perWindowState() {
|
||||
Assert.equal(panelButtonNode2.getAttribute("label"), newPerWinTitle,
|
||||
"Panel button label in new window");
|
||||
|
||||
// Disable the action in the new window.
|
||||
action.setDisabled(true, newWindow);
|
||||
Assert.equal(action.getDisabled(), false,
|
||||
"Disabled: global should remain false");
|
||||
Assert.equal(action.getDisabled(window), false,
|
||||
"Disabled: old window should remain false");
|
||||
Assert.equal(action.getDisabled(newWindow), true,
|
||||
"Disabled: new window should be true");
|
||||
|
||||
// Check PageActions.actionsInUrlbar for each window.
|
||||
Assert.deepEqual(
|
||||
PageActions.actionsInUrlbar(window).map(a => a.id),
|
||||
actionsInUrlbar.map(a => a.id),
|
||||
"PageActions.actionsInUrlbar: old window should have all actions in urlbar"
|
||||
);
|
||||
Assert.deepEqual(
|
||||
PageActions.actionsInUrlbar(newWindow).map(a => a.id),
|
||||
actionsInUrlbar.map(a => a.id).filter(id => id != action.id),
|
||||
"PageActions.actionsInUrlbar: new window should have all actions in urlbar except the test action"
|
||||
);
|
||||
|
||||
// Check the urlbar nodes for the old window.
|
||||
let actualUrlbarNodeIDs = [];
|
||||
for (let node = BrowserPageActions.mainButtonNode.nextSibling;
|
||||
node;
|
||||
node = node.nextSibling) {
|
||||
actualUrlbarNodeIDs.push(node.id);
|
||||
}
|
||||
Assert.deepEqual(
|
||||
actualUrlbarNodeIDs,
|
||||
actionsInUrlbar.map(a => BrowserPageActions.urlbarButtonNodeIDForActionID(a.id)),
|
||||
"Old window should have all nodes in urlbar"
|
||||
);
|
||||
|
||||
// Check the urlbar nodes for the new window.
|
||||
actualUrlbarNodeIDs = [];
|
||||
for (let node = newWindow.BrowserPageActions.mainButtonNode.nextSibling;
|
||||
node;
|
||||
node = node.nextSibling) {
|
||||
actualUrlbarNodeIDs.push(node.id);
|
||||
}
|
||||
Assert.deepEqual(
|
||||
actualUrlbarNodeIDs,
|
||||
actionsInUrlbar.filter(a => a.id != action.id).map(a => BrowserPageActions.urlbarButtonNodeIDForActionID(a.id)),
|
||||
"New window should have all nodes in urlbar except for the test action's"
|
||||
);
|
||||
|
||||
// Done, clean up.
|
||||
await BrowserTestUtils.closeWindow(newWindow);
|
||||
action.remove();
|
||||
@ -1056,7 +1124,7 @@ add_task(async function perWindowState() {
|
||||
// persisted state and retain its last placement in the urlbar.
|
||||
add_task(async function removeRetainState() {
|
||||
// Get the list of actions initially in the urlbar.
|
||||
let initialActionsInUrlbar = PageActions.actionsInUrlbar;
|
||||
let initialActionsInUrlbar = PageActions.actionsInUrlbar(window);
|
||||
Assert.ok(initialActionsInUrlbar.length > 0,
|
||||
"This test expects there to be at least one action in the urlbar initially (like the bookmark star)");
|
||||
|
||||
@ -1068,18 +1136,18 @@ add_task(async function removeRetainState() {
|
||||
}));
|
||||
|
||||
// Show its button in the urlbar.
|
||||
testAction.shownInUrlbar = true;
|
||||
testAction.pinnedToUrlbar = true;
|
||||
|
||||
// "Move" the test action to the front of the urlbar by toggling shownInUrlbar
|
||||
// for all the other actions in the urlbar.
|
||||
// "Move" the test action to the front of the urlbar by toggling
|
||||
// pinnedToUrlbar for all the other actions in the urlbar.
|
||||
for (let action of initialActionsInUrlbar) {
|
||||
action.shownInUrlbar = false;
|
||||
action.shownInUrlbar = true;
|
||||
action.pinnedToUrlbar = false;
|
||||
action.pinnedToUrlbar = true;
|
||||
}
|
||||
|
||||
// Check the actions in PageActions.actionsInUrlbar.
|
||||
Assert.deepEqual(
|
||||
PageActions.actionsInUrlbar.map(a => a.id),
|
||||
PageActions.actionsInUrlbar(window).map(a => a.id),
|
||||
[testAction].concat(initialActionsInUrlbar).map(a => a.id),
|
||||
"PageActions.actionsInUrlbar should be in expected order: testAction followed by all initial actions"
|
||||
);
|
||||
@ -1102,7 +1170,7 @@ add_task(async function removeRetainState() {
|
||||
|
||||
// Check the actions in PageActions.actionsInUrlbar.
|
||||
Assert.deepEqual(
|
||||
PageActions.actionsInUrlbar.map(a => a.id),
|
||||
PageActions.actionsInUrlbar(window).map(a => a.id),
|
||||
initialActionsInUrlbar.map(a => a.id),
|
||||
"PageActions.actionsInUrlbar should be in expected order after removing test action: all initial actions"
|
||||
);
|
||||
@ -1127,11 +1195,11 @@ add_task(async function removeRetainState() {
|
||||
}));
|
||||
|
||||
// Show its button in the urlbar again.
|
||||
testAction.shownInUrlbar = true;
|
||||
testAction.pinnedToUrlbar = true;
|
||||
|
||||
// Check the actions in PageActions.actionsInUrlbar.
|
||||
Assert.deepEqual(
|
||||
PageActions.actionsInUrlbar.map(a => a.id),
|
||||
PageActions.actionsInUrlbar(window).map(a => a.id),
|
||||
[testAction].concat(initialActionsInUrlbar).map(a => a.id),
|
||||
"PageActions.actionsInUrlbar should be in expected order after re-adding test action: testAction followed by all initial actions"
|
||||
);
|
||||
@ -1154,6 +1222,200 @@ add_task(async function removeRetainState() {
|
||||
});
|
||||
|
||||
|
||||
// Opens the context menu on a non-built-in action. (The context menu for
|
||||
// built-in actions is tested in browser_page_action_menu.js.)
|
||||
add_task(async function contextMenu() {
|
||||
// Add a test action.
|
||||
let action = PageActions.addAction(new PageActions.Action({
|
||||
id: "test-contextMenu",
|
||||
title: "Test contextMenu",
|
||||
pinnedToUrlbar: true,
|
||||
}));
|
||||
|
||||
// Open the panel and then open the context menu on the action's item.
|
||||
await promisePageActionPanelOpen();
|
||||
let panelButton = BrowserPageActions.panelButtonNodeForActionID(action.id);
|
||||
let contextMenuPromise = promisePanelShown("pageActionContextMenu");
|
||||
EventUtils.synthesizeMouseAtCenter(panelButton, {
|
||||
type: "contextmenu",
|
||||
button: 2,
|
||||
});
|
||||
await contextMenuPromise;
|
||||
|
||||
// The context menu should show the "don't show" item and the "manage" item.
|
||||
// Click the "don't show" item.
|
||||
let menuItems = collectContextMenuItems();
|
||||
Assert.equal(menuItems.length, 3,
|
||||
"Context menu has 3 children");
|
||||
Assert.equal(menuItems[0].label, "Don\u2019t Show in Address Bar",
|
||||
"Context menu is in the 'don't show' state");
|
||||
Assert.equal(menuItems[1].localName, "menuseparator",
|
||||
"menuseparator is present");
|
||||
Assert.equal(menuItems[2].label, "Manage Extension\u2026",
|
||||
"'Manage' item is present");
|
||||
contextMenuPromise = promisePanelHidden("pageActionContextMenu");
|
||||
EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
|
||||
await contextMenuPromise;
|
||||
|
||||
// The action should be removed from the urlbar.
|
||||
await BrowserTestUtils.waitForCondition(() => {
|
||||
return !BrowserPageActions.urlbarButtonNodeForActionID(action.id);
|
||||
}, "Waiting for urlbar button to be removed");
|
||||
|
||||
// Open the context menu again on the action's button in the panel. (The
|
||||
// panel should still be open.)
|
||||
contextMenuPromise = promisePanelShown("pageActionContextMenu");
|
||||
EventUtils.synthesizeMouseAtCenter(panelButton, {
|
||||
type: "contextmenu",
|
||||
button: 2,
|
||||
});
|
||||
await contextMenuPromise;
|
||||
|
||||
// The context menu should show the "show" item and the "manage" item. Click
|
||||
// the "show" item.
|
||||
menuItems = collectContextMenuItems();
|
||||
Assert.equal(menuItems.length, 3,
|
||||
"Context menu has 3 children");
|
||||
Assert.equal(menuItems[0].label, "Show in Address Bar",
|
||||
"Context menu is in the 'show' state");
|
||||
Assert.equal(menuItems[1].localName, "menuseparator",
|
||||
"menuseparator is present");
|
||||
Assert.equal(menuItems[2].label, "Manage Extension\u2026",
|
||||
"'Manage' item is present");
|
||||
contextMenuPromise = promisePanelHidden("pageActionContextMenu");
|
||||
EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
|
||||
await contextMenuPromise;
|
||||
|
||||
// The action should be added back to the urlbar.
|
||||
await BrowserTestUtils.waitForCondition(() => {
|
||||
return BrowserPageActions.urlbarButtonNodeForActionID(action.id);
|
||||
}, "Waiting for urlbar button to be added back");
|
||||
|
||||
// Open the context menu again on the action's button in the panel. (The
|
||||
// panel should still be open.)
|
||||
contextMenuPromise = promisePanelShown("pageActionContextMenu");
|
||||
EventUtils.synthesizeMouseAtCenter(panelButton, {
|
||||
type: "contextmenu",
|
||||
button: 2,
|
||||
});
|
||||
await contextMenuPromise;
|
||||
|
||||
// The context menu should show the "don't show" item and the "manage" item.
|
||||
// Click the "manage" item. about:addons should open.
|
||||
menuItems = collectContextMenuItems();
|
||||
Assert.equal(menuItems.length, 3,
|
||||
"Context menu has 3 children");
|
||||
Assert.equal(menuItems[0].label, "Don\u2019t Show in Address Bar",
|
||||
"Context menu is in the 'don't show' state");
|
||||
Assert.equal(menuItems[1].localName, "menuseparator",
|
||||
"menuseparator is present");
|
||||
Assert.equal(menuItems[2].label, "Manage Extension\u2026",
|
||||
"'Manage' item is present");
|
||||
contextMenuPromise = promisePanelHidden("pageActionContextMenu");
|
||||
let aboutAddonsPromise =
|
||||
BrowserTestUtils.waitForNewTab(gBrowser, "about:addons");
|
||||
EventUtils.synthesizeMouseAtCenter(menuItems[2], {});
|
||||
let values = await Promise.all([aboutAddonsPromise, contextMenuPromise]);
|
||||
let aboutAddonsTab = values[0];
|
||||
await BrowserTestUtils.removeTab(aboutAddonsTab);
|
||||
|
||||
// Open the context menu on the action's urlbar button.
|
||||
let urlbarButton = BrowserPageActions.urlbarButtonNodeForActionID(action.id);
|
||||
contextMenuPromise = promisePanelShown("pageActionContextMenu");
|
||||
EventUtils.synthesizeMouseAtCenter(urlbarButton, {
|
||||
type: "contextmenu",
|
||||
button: 2,
|
||||
});
|
||||
await contextMenuPromise;
|
||||
|
||||
// The context menu should show the "don't show" item and the "manage" item.
|
||||
// Click the "don't show" item.
|
||||
menuItems = collectContextMenuItems();
|
||||
Assert.equal(menuItems.length, 3,
|
||||
"Context menu has 3 children");
|
||||
Assert.equal(menuItems[0].label, "Don\u2019t Show in Address Bar",
|
||||
"Context menu is in the 'don't show' state");
|
||||
Assert.equal(menuItems[1].localName, "menuseparator",
|
||||
"menuseparator is present");
|
||||
Assert.equal(menuItems[2].label, "Manage Extension\u2026",
|
||||
"'Manage' item is present");
|
||||
contextMenuPromise = promisePanelHidden("pageActionContextMenu");
|
||||
EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
|
||||
await contextMenuPromise;
|
||||
|
||||
// The action should be removed from the urlbar.
|
||||
await BrowserTestUtils.waitForCondition(() => {
|
||||
return !BrowserPageActions.urlbarButtonNodeForActionID(action.id);
|
||||
}, "Waiting for urlbar button to be removed");
|
||||
|
||||
// Open the panel and then open the context menu on the action's item.
|
||||
await promisePageActionPanelOpen();
|
||||
contextMenuPromise = promisePanelShown("pageActionContextMenu");
|
||||
EventUtils.synthesizeMouseAtCenter(panelButton, {
|
||||
type: "contextmenu",
|
||||
button: 2,
|
||||
});
|
||||
await contextMenuPromise;
|
||||
|
||||
// The context menu should show the "show" item and the "manage" item. Click
|
||||
// the "show" item.
|
||||
menuItems = collectContextMenuItems();
|
||||
Assert.equal(menuItems.length, 3,
|
||||
"Context menu has 3 children");
|
||||
Assert.equal(menuItems[0].label, "Show in Address Bar",
|
||||
"Context menu is in the 'show' state");
|
||||
Assert.equal(menuItems[1].localName, "menuseparator",
|
||||
"menuseparator is present");
|
||||
Assert.equal(menuItems[2].label, "Manage Extension\u2026",
|
||||
"'Manage' item is present");
|
||||
contextMenuPromise = promisePanelHidden("pageActionContextMenu");
|
||||
EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
|
||||
await contextMenuPromise;
|
||||
|
||||
// The action should be added back to the urlbar.
|
||||
await BrowserTestUtils.waitForCondition(() => {
|
||||
return BrowserPageActions.urlbarButtonNodeForActionID(action.id);
|
||||
}, "Waiting for urlbar button to be added back");
|
||||
|
||||
// Open the context menu on the action's urlbar button.
|
||||
urlbarButton = BrowserPageActions.urlbarButtonNodeForActionID(action.id);
|
||||
contextMenuPromise = promisePanelShown("pageActionContextMenu");
|
||||
EventUtils.synthesizeMouseAtCenter(urlbarButton, {
|
||||
type: "contextmenu",
|
||||
button: 2,
|
||||
});
|
||||
await contextMenuPromise;
|
||||
|
||||
// The context menu should show the "don't show" item and the "manage" item.
|
||||
// Click the "manage" item. about:addons should open.
|
||||
menuItems = collectContextMenuItems();
|
||||
Assert.equal(menuItems.length, 3,
|
||||
"Context menu has 3 children");
|
||||
Assert.equal(menuItems[0].label, "Don\u2019t Show in Address Bar",
|
||||
"Context menu is in the 'don't show' state");
|
||||
Assert.equal(menuItems[1].localName, "menuseparator",
|
||||
"menuseparator is present");
|
||||
Assert.equal(menuItems[2].label, "Manage Extension\u2026",
|
||||
"'Manage' item is present");
|
||||
contextMenuPromise = promisePanelHidden("pageActionContextMenu");
|
||||
aboutAddonsPromise =
|
||||
BrowserTestUtils.waitForNewTab(gBrowser, "about:addons");
|
||||
EventUtils.synthesizeMouseAtCenter(menuItems[2], {});
|
||||
values = await Promise.all([aboutAddonsPromise, contextMenuPromise]);
|
||||
aboutAddonsTab = values[0];
|
||||
await BrowserTestUtils.removeTab(aboutAddonsTab);
|
||||
|
||||
// Done, clean up.
|
||||
action.remove();
|
||||
|
||||
// urlbar tests that run after this one can break if the mouse is left over
|
||||
// the area where the urlbar popup appears, which seems to happen due to the
|
||||
// above synthesized mouse events. Move it over the urlbar.
|
||||
EventUtils.synthesizeMouseAtCenter(gURLBar, { type: "mousemove" });
|
||||
gURLBar.focus();
|
||||
});
|
||||
|
||||
|
||||
function promisePageActionPanelOpen() {
|
||||
let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowUtils);
|
||||
@ -1235,3 +1497,10 @@ function promisePageActionViewChildrenVisible(panelViewNode) {
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function collectContextMenuItems() {
|
||||
let contextMenu = document.getElementById("pageActionContextMenu");
|
||||
return Array.filter(contextMenu.childNodes, node => {
|
||||
return window.getComputedStyle(node).visibility == "visible";
|
||||
});
|
||||
}
|
||||
|
@ -40,6 +40,24 @@ if CONFIG['MOZ_ARTIFACT_BUILDS']:
|
||||
'../build/prebuilt-interfaces.manifest',
|
||||
]
|
||||
|
||||
|
||||
# These defines are read in firefox.js
|
||||
DEFINES['APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
|
||||
|
||||
for cdm in CONFIG['MOZ_EME_MODULES']:
|
||||
DEFINES['MOZ_%s_EME' % cdm.upper()] = True
|
||||
|
||||
if CONFIG['MOZ_GPSD']:
|
||||
DEFINES['MOZ_GPSD'] = True
|
||||
|
||||
# These files are specified in this moz.build to pick up DIST_SUBDIR as set in
|
||||
# this directory, which is un-set in browser/app.
|
||||
JS_PREFERENCE_PP_FILES += [
|
||||
'app/profile/firefox.js',
|
||||
]
|
||||
FINAL_TARGET_FILES += ['app/blocklist.xml']
|
||||
FINAL_TARGET_FILES.defaults += ['app/permissions']
|
||||
|
||||
with Files("**"):
|
||||
BUG_COMPONENT = ("Firefox", "General")
|
||||
|
||||
|
@ -664,6 +664,7 @@ button > hbox > label {
|
||||
|
||||
.help-label {
|
||||
margin: 0 4px;
|
||||
line-height: 22px;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
|
@ -68,8 +68,6 @@
|
||||
skin/classic/browser/notification-icons/persistent-storage-blocked.svg (../shared/notification-icons/persistent-storage-blocked.svg)
|
||||
skin/classic/browser/notification-icons/persistent-storage.svg (../shared/notification-icons/persistent-storage.svg)
|
||||
skin/classic/browser/notification-icons/plugin-badge.svg (../shared/notification-icons/plugin-badge.svg)
|
||||
skin/classic/browser/notification-icons/plugin-blocked.svg (../shared/notification-icons/plugin-blocked.svg)
|
||||
skin/classic/browser/notification-icons/plugin.svg (../shared/notification-icons/plugin.svg)
|
||||
skin/classic/browser/notification-icons/popup.svg (../shared/notification-icons/popup.svg)
|
||||
skin/classic/browser/notification-icons/popup-subitem.svg (../shared/notification-icons/popup-subitem.svg)
|
||||
skin/classic/browser/notification-icons/screen-blocked.svg (../shared/notification-icons/screen-blocked.svg)
|
||||
|
@ -223,12 +223,12 @@ html|*#webRTC-previewVideo {
|
||||
/* PLUGINS */
|
||||
|
||||
.plugin-icon {
|
||||
list-style-image: url(chrome://browser/skin/notification-icons/plugin.svg);
|
||||
list-style-image: url(chrome://mozapps/skin/plugins/plugin.svg);
|
||||
transition: fill 1.5s;
|
||||
}
|
||||
|
||||
.plugin-blocked-icon {
|
||||
list-style-image: url(chrome://browser/skin/notification-icons/plugin-blocked.svg);
|
||||
list-style-image: url(chrome://mozapps/skin/plugins/plugin-blocked.svg);
|
||||
transition: fill 1.5s;
|
||||
}
|
||||
|
||||
@ -260,7 +260,7 @@ html|*#webRTC-previewVideo {
|
||||
}
|
||||
|
||||
.plugin-blocked > .plugin-icon {
|
||||
list-style-image: url(chrome://browser/skin/notification-icons/plugin-blocked.svg);
|
||||
list-style-image: url(chrome://mozapps/skin/plugins/plugin-blocked.svg);
|
||||
fill: #d92215 !important;
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
.messageImage[value="plugin-hidden"] {
|
||||
list-style-image: url(chrome://browser/skin/notification-icons/plugin.svg);
|
||||
list-style-image: url(chrome://mozapps/skin/plugins/plugin.svg);
|
||||
}
|
||||
|
||||
/* Keep any changes to this style in sync with pluginProblem.css */
|
||||
@ -10,5 +10,5 @@ notification.pluginVulnerable {
|
||||
}
|
||||
|
||||
notification.pluginVulnerable .messageImage {
|
||||
list-style-image: url(chrome://browser/skin/notification-icons/plugin-blocked.svg);
|
||||
list-style-image: url(chrome://mozapps/skin/plugins/plugin-blocked.svg);
|
||||
}
|
||||
|
@ -15,6 +15,10 @@ if CONFIG['OS_ARCH'] == 'WINNT':
|
||||
else:
|
||||
DIRS += ['unix']
|
||||
|
||||
CRAMTEST_MANIFESTS += [
|
||||
'tests/cram/cram.ini',
|
||||
]
|
||||
|
||||
if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'android':
|
||||
DIRS += ['annotationProcessors']
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
# PGO
|
||||
# ==============================================================
|
||||
option(env='MOZ_PGO', help='Build with profile guided optimizations')
|
||||
js_option(env='MOZ_PGO', help='Build with profile guided optimizations')
|
||||
|
||||
set_config('MOZ_PGO', depends('MOZ_PGO')(lambda x: bool(x)))
|
||||
add_old_configure_assignment('MOZ_PGO', depends('MOZ_PGO')(lambda x: bool(x)))
|
||||
|
1
build/tests/cram/cram.ini
Normal file
1
build/tests/cram/cram.ini
Normal file
@ -0,0 +1 @@
|
||||
[test_configure_help.t]
|
14
build/tests/cram/test_configure_help.t
Normal file
14
build/tests/cram/test_configure_help.t
Normal file
@ -0,0 +1,14 @@
|
||||
configure --help works
|
||||
|
||||
$ cd $TESTDIR/../../..
|
||||
|
||||
$ touch $TMP/mozconfig
|
||||
$ export MOZCONFIG=$TMP/mozconfig
|
||||
$ ./configure --help 2>& 1 | head -n 7
|
||||
Adding configure options from */tmp/mozconfig (glob)
|
||||
checking for vcs source checkout... hg
|
||||
checking for vcs source checkout... hg
|
||||
Usage: configure.py [options]
|
||||
|
||||
Options: [defaults in brackets after descriptions]
|
||||
--help print this message
|
@ -56,8 +56,8 @@ function testScriptSrc(aCallback) {
|
||||
|
||||
/** <img src=""> tests **/
|
||||
var img_global = "chrome://global/skin/media/error.png";
|
||||
var img_mozapps = "chrome://mozapps/skin/plugins/contentPluginClose.png";
|
||||
var res_mozapps = "resource://gre/chrome/toolkit/skin/classic/mozapps/plugins/contentPluginClose.png";
|
||||
var img_mozapps = "chrome://mozapps/skin/plugins/contentPluginCrashed.png";
|
||||
var res_mozapps = "resource://gre/chrome/toolkit/skin/classic/mozapps/plugins/contentPluginCrashed.png";
|
||||
|
||||
var imgTests = [[img_global, "success"],
|
||||
[img_mozapps, "fail"],
|
||||
|
@ -119,6 +119,11 @@ OBJDIR_TARGETS = install export libs clean realclean distclean upload sdk instal
|
||||
# The default rule is build
|
||||
build::
|
||||
|
||||
ifndef MACH
|
||||
$(error client.mk must be used via `mach`. Try running \
|
||||
`./mach $(firstword $(MAKECMDGOALS) $(.DEFAULT_GOAL))`)
|
||||
endif
|
||||
|
||||
# Include baseconfig.mk for its $(MAKE) validation.
|
||||
include $(TOPSRCDIR)/config/baseconfig.mk
|
||||
|
||||
|
@ -32,53 +32,6 @@ endif
|
||||
|
||||
include $(topsrcdir)/config/rules.mk
|
||||
|
||||
ifdef WRAP_SYSTEM_INCLUDES
|
||||
export-preqs = \
|
||||
$(call mkdir_deps,system_wrappers) \
|
||||
$(NULL)
|
||||
|
||||
export:: $(export-preqs)
|
||||
$(PYTHON) -m mozbuild.action.preprocessor $(DEFINES) $(ACDEFINES) \
|
||||
-DMOZ_TREE_CAIRO=$(MOZ_TREE_CAIRO) \
|
||||
-DMOZ_TREE_PIXMAN=$(MOZ_TREE_PIXMAN) \
|
||||
-DMOZ_SYSTEM_HUNSPELL=$(MOZ_SYSTEM_HUNSPELL) \
|
||||
-DMOZ_SYSTEM_BZ2=$(MOZ_SYSTEM_BZ2) \
|
||||
-DMOZ_SYSTEM_ZLIB=$(MOZ_SYSTEM_ZLIB) \
|
||||
-DMOZ_SYSTEM_PNG=$(MOZ_SYSTEM_PNG) \
|
||||
-DMOZ_SYSTEM_JPEG=$(MOZ_SYSTEM_JPEG) \
|
||||
-DMOZ_SYSTEM_LIBEVENT=$(MOZ_SYSTEM_LIBEVENT) \
|
||||
-DMOZ_SYSTEM_LIBVPX=$(MOZ_SYSTEM_LIBVPX) \
|
||||
-DMOZ_SYSTEM_ICU=$(MOZ_SYSTEM_ICU) \
|
||||
$(srcdir)/system-headers $(srcdir)/stl-headers | $(PERL) $(topsrcdir)/nsprpub/config/make-system-wrappers.pl system_wrappers
|
||||
$(INSTALL) system_wrappers $(DIST)
|
||||
|
||||
GARBAGE_DIRS += system_wrappers
|
||||
endif
|
||||
|
||||
ifdef WRAP_STL_INCLUDES
|
||||
ifdef GNU_CXX
|
||||
stl_compiler = gcc
|
||||
else
|
||||
ifdef _MSC_VER
|
||||
stl_compiler = msvc
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
ifdef stl_compiler
|
||||
STL_WRAPPERS_SENTINEL = $(DIST)/stl_wrappers/sentinel
|
||||
|
||||
$(STL_WRAPPERS_SENTINEL): $(srcdir)/make-stl-wrappers.py $(srcdir)/$(stl_compiler)-stl-wrapper.template.h $(srcdir)/stl-headers $(GLOBAL_DEPS)
|
||||
$(PYTHON) $(srcdir)/make-stl-wrappers.py stl_wrappers $(stl_compiler) $(srcdir)/$(stl_compiler)-stl-wrapper.template.h $(srcdir)/stl-headers
|
||||
$(PYTHON) $(srcdir)/nsinstall.py -t stl_wrappers $(DIST)
|
||||
touch $(STL_WRAPPERS_SENTINEL)
|
||||
|
||||
export:: $(STL_WRAPPERS_SENTINEL)
|
||||
|
||||
GARBAGE += $(STL_WRAPPERS_SENTINEL)
|
||||
GARBAGE_DIRS += stl_wrappers
|
||||
endif
|
||||
|
||||
GARBAGE += \
|
||||
$(FINAL_LINK_COMPS) $(FINAL_LINK_LIBS) $(FINAL_LINK_COMP_NAMES) $(srcdir)/*.pyc *.pyc
|
||||
|
||||
|
@ -22,31 +22,13 @@ def header_path(header, compiler):
|
||||
# hope someone notices this ...
|
||||
raise NotImplementedError(compiler)
|
||||
|
||||
def is_comment(line):
|
||||
return re.match(r'\s*#.*', line)
|
||||
|
||||
def main(outdir, compiler, template_file, header_list_file):
|
||||
if not os.path.isdir(outdir):
|
||||
os.mkdir(outdir)
|
||||
|
||||
# The 'unused' arg is the output file from the file_generate action. We actually
|
||||
# generate all the files in header_list
|
||||
def gen_wrappers(unused, outdir, compiler, template_file, *header_list):
|
||||
template = open(template_file, 'r').read()
|
||||
|
||||
for header in open(header_list_file, 'r'):
|
||||
header = header.rstrip()
|
||||
if 0 == len(header) or is_comment(header):
|
||||
continue
|
||||
|
||||
for header in header_list:
|
||||
path = header_path(header, compiler)
|
||||
with FileAvoidWrite(os.path.join(outdir, header)) as f:
|
||||
f.write(string.Template(template).substitute(HEADER=header,
|
||||
HEADER_PATH=path))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if 5 != len(sys.argv):
|
||||
print("""Usage:
|
||||
python {0} OUT_DIR ('msvc'|'gcc') TEMPLATE_FILE HEADER_LIST_FILE
|
||||
""".format(sys.argv[0]), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
main(*sys.argv[1:])
|
||||
|
23
config/make-system-wrappers.py
Normal file
23
config/make-system-wrappers.py
Normal file
@ -0,0 +1,23 @@
|
||||
# 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/.
|
||||
from __future__ import print_function
|
||||
import os
|
||||
import sys
|
||||
from mozbuild.util import FileAvoidWrite
|
||||
|
||||
header_template = '''#pragma GCC system_header
|
||||
#pragma GCC visibility push(default)
|
||||
#include_next <%(header)s>
|
||||
#pragma GCC visibility pop
|
||||
'''
|
||||
|
||||
|
||||
# The 'unused' arg is the output file from the file_generate action. We actually
|
||||
# generate all the files in header_list
|
||||
def gen_wrappers(unused, outdir, *header_list):
|
||||
for header in header_list:
|
||||
with FileAvoidWrite(os.path.join(outdir, header)) as f:
|
||||
f.write(header_template % {
|
||||
'header': header,
|
||||
})
|
@ -42,3 +42,35 @@ HOST_DEFINES = {
|
||||
'UNICODE': True,
|
||||
'_UNICODE': True,
|
||||
}
|
||||
|
||||
include('stl-headers.mozbuild')
|
||||
if CONFIG['WRAP_STL_INCLUDES']:
|
||||
stl_compiler = None
|
||||
if CONFIG['GNU_CXX']:
|
||||
stl_compiler = 'gcc'
|
||||
elif CONFIG['_MSC_VER']:
|
||||
stl_compiler = 'msvc'
|
||||
|
||||
if stl_compiler:
|
||||
template_file = SRCDIR + '/%s-stl-wrapper.template.h' % stl_compiler
|
||||
output_dir = '../dist/stl_wrappers'
|
||||
# We have to use a sentinel file as the first file because the
|
||||
# file_generate action will create it for us, but we want to create all
|
||||
# the files in gen_wrappers()
|
||||
outputs = tuple(['stl.sentinel'] + ['%s/%s' % (output_dir, h) for h in stl_headers])
|
||||
GENERATED_FILES += [outputs]
|
||||
stl = GENERATED_FILES[outputs]
|
||||
stl.script = 'make-stl-wrappers.py:gen_wrappers'
|
||||
stl.flags = [output_dir, stl_compiler, template_file]
|
||||
stl.flags.extend(stl_headers)
|
||||
|
||||
if CONFIG['WRAP_SYSTEM_INCLUDES']:
|
||||
include('system-headers.mozbuild')
|
||||
output_dir = '../dist/system_wrappers'
|
||||
outputs = tuple(['system-header.sentinel'] + ['%s/%s' % (output_dir, h) for h in stl_headers + system_headers])
|
||||
GENERATED_FILES += [outputs]
|
||||
system = GENERATED_FILES[outputs]
|
||||
system.script = 'make-system-wrappers.py:gen_wrappers'
|
||||
system.flags = [output_dir]
|
||||
system.flags.extend(stl_headers)
|
||||
system.flags.extend(system_headers)
|
||||
|
@ -1,52 +0,0 @@
|
||||
#
|
||||
# This file contains a list the of STL headers that have been reviewed
|
||||
# for exception safety and approved. See
|
||||
#
|
||||
# https://bugzilla.mozilla.org/show_bug.cgi?id=551254
|
||||
#
|
||||
# At build time, each header listed here is converted into a "wrapper
|
||||
# header" that is installed into dist/stl_includes.
|
||||
#
|
||||
# If you would like to request a new STL header <foo> be added, please
|
||||
# file a Core:XPCOM bug with a title like "STL: Review exception
|
||||
# safety of <foo> for gcc and MSVC".
|
||||
#
|
||||
|
||||
new
|
||||
|
||||
# FIXME: these headers haven't been reviewed yet, but we use them
|
||||
# unsafely in Gecko, so we might as well prevent them from
|
||||
# throwing exceptions
|
||||
algorithm
|
||||
atomic
|
||||
deque
|
||||
functional
|
||||
ios
|
||||
iosfwd
|
||||
iostream
|
||||
istream
|
||||
iterator
|
||||
limits
|
||||
list
|
||||
map
|
||||
memory
|
||||
ostream
|
||||
set
|
||||
stack
|
||||
string
|
||||
thread
|
||||
type_traits
|
||||
unordered_map
|
||||
unordered_set
|
||||
utility
|
||||
vector
|
||||
cassert
|
||||
climits
|
||||
cmath
|
||||
cstdarg
|
||||
cstdio
|
||||
cstdlib
|
||||
cstring
|
||||
cwchar
|
||||
tuple
|
||||
xutility
|
57
config/stl-headers.mozbuild
Normal file
57
config/stl-headers.mozbuild
Normal file
@ -0,0 +1,57 @@
|
||||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
# This list contains the of STL headers that have been reviewed for exception
|
||||
# safety and approved. See
|
||||
#
|
||||
# https://bugzilla.mozilla.org/show_bug.cgi?id=551254
|
||||
#
|
||||
# At build time, each header listed here is converted into a "wrapper
|
||||
# header" that is installed into dist/stl_includes.
|
||||
#
|
||||
# If you would like to request a new STL header <foo> be added, please
|
||||
# file a Core:XPCOM bug with a title like "STL: Review exception
|
||||
# safety of <foo> for gcc and MSVC".
|
||||
stl_headers = [
|
||||
'new',
|
||||
|
||||
# FIXME: these headers haven't been reviewed yet, but we use them
|
||||
# unsafely in Gecko, so we might as well prevent them from
|
||||
# throwing exceptions
|
||||
'algorithm',
|
||||
'atomic',
|
||||
'deque',
|
||||
'functional',
|
||||
'ios',
|
||||
'iosfwd',
|
||||
'iostream',
|
||||
'istream',
|
||||
'iterator',
|
||||
'limits',
|
||||
'list',
|
||||
'map',
|
||||
'memory',
|
||||
'ostream',
|
||||
'set',
|
||||
'stack',
|
||||
'string',
|
||||
'thread',
|
||||
'type_traits',
|
||||
'unordered_map',
|
||||
'unordered_set',
|
||||
'utility',
|
||||
'vector',
|
||||
'cassert',
|
||||
'climits',
|
||||
'cmath',
|
||||
'cstdarg',
|
||||
'cstdio',
|
||||
'cstdlib',
|
||||
'cstring',
|
||||
'cwchar',
|
||||
'tuple',
|
||||
'xutility',
|
||||
]
|
File diff suppressed because it is too large
Load Diff
1346
config/system-headers.mozbuild
Normal file
1346
config/system-headers.mozbuild
Normal file
File diff suppressed because it is too large
Load Diff
@ -62,6 +62,55 @@ add_task(async function () {
|
||||
await assertRowSelected(numRows - 1);
|
||||
});
|
||||
|
||||
add_task(async function () {
|
||||
info("Test 3 JSON row selection started");
|
||||
|
||||
// Create a JSON with a row taller than the panel.
|
||||
let json = JSON.stringify([0, "a ".repeat(1e4), 1]);
|
||||
await addJsonViewTab("data:application/json," + encodeURI(json));
|
||||
|
||||
is(await getElementCount(".treeRow"), 3, "Got the expected number of rows.");
|
||||
await assertRowSelected(null);
|
||||
await evalInContent("var scroller = $('.jsonPanelBox .panelContent')");
|
||||
await evalInContent("var row = $('.treeRow:nth-child(2)')");
|
||||
ok(await evalInContent("scroller.clientHeight < row.clientHeight"),
|
||||
"The row is taller than the scroller.");
|
||||
is(await evalInContent("scroller.scrollTop"), 0, "Initially scrolled to the top.");
|
||||
|
||||
// Select the tall row.
|
||||
await evalInContent("row.click()");
|
||||
await assertRowSelected(2);
|
||||
is(await evalInContent("scroller.scrollTop"), await evalInContent("row.offsetTop"),
|
||||
"Scrolled to the top of the row.");
|
||||
|
||||
// Select the last row.
|
||||
await evalInContent("$('.treeRow:last-child').click()");
|
||||
await assertRowSelected(3);
|
||||
is(await evalInContent("scroller.scrollTop + scroller.offsetHeight"),
|
||||
await evalInContent("scroller.scrollHeight"), "Scrolled to the bottom.");
|
||||
|
||||
// Select the tall row.
|
||||
await evalInContent("row.click()");
|
||||
await assertRowSelected(2);
|
||||
is(await evalInContent("scroller.scrollTop + scroller.offsetHeight"),
|
||||
await evalInContent("row.offsetTop + row.offsetHeight"),
|
||||
"Scrolled to the bottom of the row.");
|
||||
|
||||
// Scroll up a bit, so that both the top and bottom of the row are not visible.
|
||||
let scroll = await evalInContent(
|
||||
"scroller.scrollTop = Math.ceil((scroller.scrollTop + row.offsetTop) / 2)");
|
||||
ok(await evalInContent("scroller.scrollTop > row.offsetTop"),
|
||||
"The top of the row is not visible.");
|
||||
ok(await evalInContent("scroller.scrollTop + scroller.offsetHeight")
|
||||
< await evalInContent("row.offsetTop + row.offsetHeight"),
|
||||
"The bottom of the row is not visible.");
|
||||
|
||||
// Select the tall row.
|
||||
await evalInContent("row.click()");
|
||||
await assertRowSelected(2);
|
||||
is(await evalInContent("scroller.scrollTop"), scroll, "Scroll did not change");
|
||||
});
|
||||
|
||||
async function assertRowSelected(rowNum) {
|
||||
let idx = evalInContent("[].indexOf.call($$('.treeRow'), $('.treeRow.selected'))");
|
||||
is(await idx + 1, +rowNum, `${rowNum ? "The row #" + rowNum : "No row"} is selected.`);
|
||||
|
@ -321,13 +321,7 @@ charts.totalSize=Size: %S KB
|
||||
# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
|
||||
# This is the label displayed in the performance analysis view for the
|
||||
# total requests time, in seconds.
|
||||
charts.totalSeconds=Time: %1$S second;Time: %1$S seconds
|
||||
|
||||
# LOCALIZATION NOTE (charts.totalSecondsNonBlocking): Semi-colon list of plural forms.
|
||||
# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
|
||||
# This is the label displayed in the performance analysis view for the
|
||||
# total requests time (non-blocking), in seconds.
|
||||
charts.totalSecondsNonBlocking=Non blocking time: %1$S second;Non blocking time: %1$S seconds
|
||||
charts.totalSeconds=Time: #1 second;Time: #1 seconds
|
||||
|
||||
# LOCALIZATION NOTE (charts.totalCached): This is the label displayed
|
||||
# in the performance analysis view for total cached responses.
|
||||
@ -354,11 +348,6 @@ charts.transferred=Transferred
|
||||
# in the header column in the performance analysis view for time of request.
|
||||
charts.time=Time
|
||||
|
||||
# LOCALIZATION NOTE (charts.nonBlockingTime): This is the label displayed
|
||||
# in the header column in the performance analysis view for non blocking
|
||||
# time of request.
|
||||
charts.nonBlockingTime=Non blocking time
|
||||
|
||||
# LOCALIZATION NOTE (netRequest.headers): A label used for Headers tab
|
||||
# This tab displays list of HTTP headers
|
||||
netRequest.headers=Headers
|
||||
|
@ -132,7 +132,6 @@ class StatisticsPanel extends Component {
|
||||
size: L10N.getStr("charts.size"),
|
||||
transferredSize: L10N.getStr("charts.transferred"),
|
||||
time: L10N.getStr("charts.time"),
|
||||
nonBlockingTime: L10N.getStr("charts.nonBlockingTime"),
|
||||
},
|
||||
data,
|
||||
strings: {
|
||||
@ -143,8 +142,6 @@ class StatisticsPanel extends Component {
|
||||
getSizeWithDecimals(value / 1024)),
|
||||
time: (value) =>
|
||||
L10N.getFormatStr("charts.totalS", getTimeWithDecimals(value / 1000)),
|
||||
nonBlockingTime: (value) =>
|
||||
L10N.getFormatStr("charts.totalS", getTimeWithDecimals(value / 1000)),
|
||||
},
|
||||
totals: {
|
||||
cached: (total) => L10N.getFormatStr("charts.totalCached", total),
|
||||
@ -158,13 +155,7 @@ class StatisticsPanel extends Component {
|
||||
let seconds = total / 1000;
|
||||
let string = getTimeWithDecimals(seconds);
|
||||
return PluralForm.get(seconds,
|
||||
L10N.getFormatStr("charts.totalSeconds", string));
|
||||
},
|
||||
nonBlockingTime: (total) => {
|
||||
let seconds = total / 1000;
|
||||
let string = getTimeWithDecimals(seconds);
|
||||
return PluralForm.get(seconds,
|
||||
L10N.getFormatStr("charts.totalSecondsNonBlocking", string));
|
||||
L10N.getStr("charts.totalSeconds")).replace("#1", string);
|
||||
},
|
||||
},
|
||||
sorted: true,
|
||||
@ -194,7 +185,6 @@ class StatisticsPanel extends Component {
|
||||
size: 0,
|
||||
transferredSize: 0,
|
||||
time: 0,
|
||||
nonBlockingTime: 0,
|
||||
}));
|
||||
|
||||
for (let request of requests) {
|
||||
@ -234,9 +224,6 @@ class StatisticsPanel extends Component {
|
||||
data[type].time += request.totalTime || 0;
|
||||
data[type].size += request.contentSize || 0;
|
||||
data[type].transferredSize += request.transferredSize || 0;
|
||||
let nonBlockingTime =
|
||||
request.eventTimings.totalTime - request.eventTimings.timings.blocked;
|
||||
data[type].nonBlockingTime += nonBlockingTime || 0;
|
||||
} else {
|
||||
data[type].cached++;
|
||||
}
|
||||
|
@ -287,7 +287,19 @@ define(function (require, exports, module) {
|
||||
this.setState(Object.assign({}, this.state, {
|
||||
selected: row.id
|
||||
}));
|
||||
row.scrollIntoView({block: "nearest"});
|
||||
|
||||
// If the top or bottom side of the row is not visible and there is available space
|
||||
// beyond the opposite one, then attempt to scroll the hidden side into view, but
|
||||
// without hiding the visible side.
|
||||
let scroller = scrollContainer(row);
|
||||
if (!scroller) {
|
||||
return;
|
||||
}
|
||||
let scrollToTop = row.offsetTop;
|
||||
let scrollToBottom = scrollToTop + row.offsetHeight - scroller.offsetHeight;
|
||||
let max = Math.max(scrollToTop, scrollToBottom);
|
||||
let min = Math.min(scrollToTop, scrollToBottom);
|
||||
scroller.scrollTop = Math.max(min, Math.min(max, scroller.scrollTop));
|
||||
}
|
||||
|
||||
isSelected(nodePath) {
|
||||
@ -502,6 +514,18 @@ define(function (require, exports, module) {
|
||||
return typeof value == "string" && value.length > 50;
|
||||
}
|
||||
|
||||
function scrollContainer(element) {
|
||||
let parent = element.parentElement;
|
||||
let window = element.ownerDocument.defaultView;
|
||||
if (!parent || !window) {
|
||||
return null;
|
||||
}
|
||||
if (window.getComputedStyle(parent).overflowY != "visible") {
|
||||
return parent;
|
||||
}
|
||||
return scrollContainer(parent);
|
||||
}
|
||||
|
||||
// Exports from this module
|
||||
module.exports = TreeView;
|
||||
});
|
||||
|
@ -23,6 +23,12 @@ const MESSAGES = {
|
||||
SystemMenu: "menu-message",
|
||||
};
|
||||
|
||||
// Google analytics parameters that should be added to all outgoing links.
|
||||
const GA_PARAMETERS = [
|
||||
["utm_source", "devtools"],
|
||||
["utm_medium", "onboarding"],
|
||||
];
|
||||
|
||||
const ABOUTDEVTOOLS_STRINGS = "chrome://devtools-shim/locale/aboutdevtools.properties";
|
||||
const aboutDevtoolsBundle = Services.strings.createBundle(ABOUTDEVTOOLS_STRINGS);
|
||||
|
||||
@ -186,6 +192,14 @@ window.addEventListener("load", function () {
|
||||
featuresContainer.appendChild(createFeatureEl(feature));
|
||||
}
|
||||
|
||||
// Add Google Analytics parameters to all the external links.
|
||||
let externalLinks = [...document.querySelectorAll("a.external")];
|
||||
for (let link of externalLinks) {
|
||||
let linkUrl = new URL(link.getAttribute("href"));
|
||||
GA_PARAMETERS.forEach(([key, value]) => linkUrl.searchParams.set(key, value));
|
||||
link.setAttribute("href", linkUrl.href);
|
||||
}
|
||||
|
||||
// Update the current page based on the current value of DEVTOOLS_ENABLED_PREF.
|
||||
updatePage();
|
||||
|
||||
|
@ -409,8 +409,54 @@ public:
|
||||
template<typename A, typename B>
|
||||
bool operator==(const RangeBoundaryBase<A, B>& aOther) const
|
||||
{
|
||||
return mParent == aOther.mParent &&
|
||||
(mRef ? mRef == aOther.mRef : mOffset == aOther.mOffset);
|
||||
if (mParent != aOther.mParent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mOffset.isSome() && aOther.mOffset.isSome()) {
|
||||
// If both mOffset are set, we need to compare both mRef too because
|
||||
// the relation of mRef and mOffset have already broken by DOM tree
|
||||
// changes.
|
||||
if (mOffset != aOther.mOffset) {
|
||||
return false;
|
||||
}
|
||||
if (mRef == aOther.mRef) {
|
||||
return true;
|
||||
}
|
||||
if (NS_WARN_IF(mRef && aOther.mRef)) {
|
||||
// In this case, relation between mRef and mOffset of one of or both of
|
||||
// them doesn't match with current DOM tree since the DOM tree might
|
||||
// have been changed after computing mRef or mOffset.
|
||||
return false;
|
||||
}
|
||||
// If one of mRef hasn't been computed yet, we should compare them only
|
||||
// with mOffset. Perhaps, we shouldn't copy mRef from non-nullptr one to
|
||||
// nullptr one since if we copy it here, it may be unexpected behavior for
|
||||
// some callers.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mOffset.isSome() && !mRef &&
|
||||
!aOther.mOffset.isSome() && aOther.mRef) {
|
||||
// If this has only mOffset and the other has only mRef, this needs to
|
||||
// compute mRef now.
|
||||
EnsureRef();
|
||||
return mRef == aOther.mRef;
|
||||
}
|
||||
|
||||
if (!mOffset.isSome() && mRef &&
|
||||
aOther.mOffset.isSome() && !aOther.mRef) {
|
||||
// If this has only mRef and the other has only mOffset, the other needs
|
||||
// to compute mRef now.
|
||||
aOther.EnsureRef();
|
||||
return mRef == aOther.mRef;
|
||||
}
|
||||
|
||||
// If mOffset of one of them hasn't been computed from mRef yet, we should
|
||||
// compare only with mRef. Perhaps, we shouldn't copy mOffset from being
|
||||
// some one to not being some one since if we copy it here, it may be
|
||||
// unexpected behavior for some callers.
|
||||
return mRef == aOther.mRef;
|
||||
}
|
||||
|
||||
template<typename A, typename B>
|
||||
|
@ -1737,12 +1737,6 @@ void HTMLMediaElement::ShutdownDecoder()
|
||||
|
||||
void HTMLMediaElement::AbortExistingLoads()
|
||||
{
|
||||
// If there is no existing decoder then we don't have anything to
|
||||
// report. This prevents reporting the initial load from an
|
||||
// empty video element as a failed EME load.
|
||||
if (mDecoder) {
|
||||
ReportEMETelemetry();
|
||||
}
|
||||
// Abort any already-running instance of the resource selection algorithm.
|
||||
mLoadWaitStatus = NOT_WAITING;
|
||||
|
||||
@ -4590,18 +4584,6 @@ void HTMLMediaElement::HiddenVideoStop()
|
||||
mVideoDecodeSuspendTimer = nullptr;
|
||||
}
|
||||
|
||||
void
|
||||
HTMLMediaElement::ReportEMETelemetry()
|
||||
{
|
||||
// Report telemetry for EME videos when a page is unloaded.
|
||||
NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
|
||||
if (mIsEncrypted && Preferences::GetBool("media.eme.enabled")) {
|
||||
Telemetry::Accumulate(Telemetry::VIDEO_EME_PLAY_SUCCESS, mLoadedDataFired);
|
||||
LOG(LogLevel::Debug, ("%p VIDEO_EME_PLAY_SUCCESS = %s",
|
||||
this, mLoadedDataFired ? "true" : "false"));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
HTMLMediaElement::ReportTelemetry()
|
||||
{
|
||||
@ -6463,7 +6445,6 @@ void HTMLMediaElement::SuspendOrResumeElement(bool aPauseElement, bool aSuspendE
|
||||
UpdateAudioChannelPlayingState();
|
||||
if (aPauseElement) {
|
||||
ReportTelemetry();
|
||||
ReportEMETelemetry();
|
||||
|
||||
// For EME content, we may force destruction of the CDM client (and CDM
|
||||
// instance if this is the last client for that CDM instance) and
|
||||
|
@ -1241,8 +1241,6 @@ protected:
|
||||
*/
|
||||
void HiddenVideoStop();
|
||||
|
||||
void ReportEMETelemetry();
|
||||
|
||||
void ReportTelemetry();
|
||||
|
||||
// Seeks to aTime seconds. aSeekType can be Exact to seek to exactly the
|
||||
|
@ -29,9 +29,8 @@ ChannelMediaResource::ChannelMediaResource(MediaResourceCallback* aCallback,
|
||||
nsIURI* aURI,
|
||||
bool aIsPrivateBrowsing)
|
||||
: BaseMediaResource(aCallback, aChannel, aURI)
|
||||
, mReopenOnError(false)
|
||||
, mCacheStream(this, aIsPrivateBrowsing)
|
||||
, mSuspendAgent(mChannel, mCacheStream)
|
||||
, mSuspendAgent(mCacheStream)
|
||||
{
|
||||
}
|
||||
|
||||
@ -41,10 +40,9 @@ ChannelMediaResource::ChannelMediaResource(
|
||||
nsIURI* aURI,
|
||||
const MediaChannelStatistics& aStatistics)
|
||||
: BaseMediaResource(aCallback, aChannel, aURI)
|
||||
, mReopenOnError(false)
|
||||
, mCacheStream(this, /* aIsPrivateBrowsing = */ false)
|
||||
, mChannelStatistics(aStatistics)
|
||||
, mSuspendAgent(mChannel, mCacheStream)
|
||||
, mSuspendAgent(mCacheStream)
|
||||
{
|
||||
}
|
||||
|
||||
@ -86,7 +84,7 @@ ChannelMediaResource::Listener::OnStopRequest(nsIRequest* aRequest,
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
if (!mResource)
|
||||
return NS_OK;
|
||||
return mResource->OnStopRequest(aRequest, aStatus);
|
||||
return mResource->OnStopRequest(aRequest, aStatus, mReopenOnError);
|
||||
}
|
||||
|
||||
nsresult
|
||||
@ -296,9 +294,8 @@ ChannelMediaResource::OnStartRequest(nsIRequest* aRequest,
|
||||
mCacheStream.NotifyDataStarted(mLoadID, startOffset, seekable);
|
||||
mIsTransportSeekable = seekable;
|
||||
mChannelStatistics.Start();
|
||||
mReopenOnError = false;
|
||||
|
||||
mSuspendAgent.UpdateSuspendedStatusIfNeeded();
|
||||
mSuspendAgent.Delegate(mChannel);
|
||||
|
||||
// Fires an initial progress event.
|
||||
owner->DownloadProgressed();
|
||||
@ -371,7 +368,9 @@ ChannelMediaResource::ParseContentRangeHeader(nsIHttpChannel * aHttpChan,
|
||||
}
|
||||
|
||||
nsresult
|
||||
ChannelMediaResource::OnStopRequest(nsIRequest* aRequest, nsresult aStatus)
|
||||
ChannelMediaResource::OnStopRequest(nsIRequest* aRequest,
|
||||
nsresult aStatus,
|
||||
bool aReopenOnError)
|
||||
{
|
||||
NS_ASSERTION(mChannel.get() == aRequest, "Wrong channel!");
|
||||
NS_ASSERTION(!mSuspendAgent.IsSuspended(),
|
||||
@ -386,7 +385,7 @@ ChannelMediaResource::OnStopRequest(nsIRequest* aRequest, nsresult aStatus)
|
||||
// cases where we don't need to reopen are when *we* closed the stream.
|
||||
// But don't reopen if we need to seek and we don't think we can... that would
|
||||
// cause us to just re-read the stream, which would be really bad.
|
||||
if (mReopenOnError && aStatus != NS_ERROR_PARSED_DATA_CACHED &&
|
||||
if (aReopenOnError && aStatus != NS_ERROR_PARSED_DATA_CACHED &&
|
||||
aStatus != NS_BINDING_ABORTED &&
|
||||
(GetOffset() == 0 || (GetLength() > 0 && GetOffset() != GetLength() &&
|
||||
mIsTransportSeekable))) {
|
||||
@ -425,8 +424,9 @@ ChannelMediaResource::OnChannelRedirect(nsIChannel* aOld,
|
||||
uint32_t aFlags,
|
||||
int64_t aOffset)
|
||||
{
|
||||
// OnChannelRedirect() is followed by OnStartRequest() where we will
|
||||
// call mSuspendAgent.Delegate().
|
||||
mChannel = aNew;
|
||||
mSuspendAgent.NotifyChannelOpened(mChannel);
|
||||
return SetupChannelHeaders(aOffset);
|
||||
}
|
||||
|
||||
@ -626,7 +626,7 @@ void ChannelMediaResource::CloseChannel()
|
||||
mChannelStatistics.Stop();
|
||||
|
||||
if (mChannel) {
|
||||
mSuspendAgent.NotifyChannelClosing();
|
||||
mSuspendAgent.Revoke();
|
||||
// The status we use here won't be passed to the decoder, since
|
||||
// we've already revoked the listener. It can however be passed
|
||||
// to nsDocumentViewer::LoadComplete if our channel is the one
|
||||
@ -730,7 +730,7 @@ ChannelMediaResource::Resume()
|
||||
mChannelStatistics.Start();
|
||||
// if an error occurs after Resume, assume it's because the server
|
||||
// timed out the connection and we should reopen it.
|
||||
mReopenOnError = true;
|
||||
mListener->SetReopenOnError();
|
||||
element->DownloadResumed();
|
||||
} else {
|
||||
int64_t totalLength = GetLength();
|
||||
@ -818,8 +818,6 @@ ChannelMediaResource::RecreateChannel()
|
||||
cos->AddClassFlags(nsIClassOfService::DontThrottle);
|
||||
}
|
||||
|
||||
mSuspendAgent.NotifyChannelOpened(mChannel);
|
||||
|
||||
// Tell the cache to reset the download status when the channel is reopened.
|
||||
mCacheStream.NotifyChannelRecreated();
|
||||
|
||||
@ -1067,28 +1065,31 @@ ChannelSuspendAgent::Resume()
|
||||
}
|
||||
|
||||
void
|
||||
ChannelSuspendAgent::UpdateSuspendedStatusIfNeeded()
|
||||
ChannelSuspendAgent::Delegate(nsIChannel* aChannel)
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
if (!mIsChannelSuspended && IsSuspended()) {
|
||||
MOZ_ASSERT(aChannel);
|
||||
MOZ_ASSERT(!mChannel, "The previous channel not closed.");
|
||||
MOZ_ASSERT(!mIsChannelSuspended);
|
||||
|
||||
mChannel = aChannel;
|
||||
// Ensure the suspend status of the channel matches our suspend count.
|
||||
if (IsSuspended()) {
|
||||
SuspendInternal();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
ChannelSuspendAgent::NotifyChannelOpened(nsIChannel* aChannel)
|
||||
ChannelSuspendAgent::Revoke()
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
MOZ_ASSERT(aChannel);
|
||||
mChannel = aChannel;
|
||||
}
|
||||
|
||||
void
|
||||
ChannelSuspendAgent::NotifyChannelClosing()
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
MOZ_ASSERT(mChannel);
|
||||
// Before close the channel, it need to be resumed to make sure its internal
|
||||
if (!mChannel) {
|
||||
// Channel already revoked. Nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
// Before closing the channel, it needs to be resumed to make sure its internal
|
||||
// state is correct. Besides, We need to suspend the channel after recreating.
|
||||
if (mIsChannelSuspended) {
|
||||
mChannel->Resume();
|
||||
@ -1104,5 +1105,4 @@ ChannelSuspendAgent::IsSuspended()
|
||||
return (mSuspendCount > 0);
|
||||
}
|
||||
|
||||
|
||||
} // mozilla namespace
|
||||
|
@ -24,9 +24,8 @@ namespace mozilla {
|
||||
class ChannelSuspendAgent
|
||||
{
|
||||
public:
|
||||
ChannelSuspendAgent(nsIChannel* aChannel, MediaCacheStream& aCacheStream)
|
||||
: mChannel(aChannel)
|
||||
, mCacheStream(aCacheStream)
|
||||
explicit ChannelSuspendAgent(MediaCacheStream& aCacheStream)
|
||||
: mCacheStream(aCacheStream)
|
||||
{
|
||||
}
|
||||
|
||||
@ -40,21 +39,16 @@ public:
|
||||
// Return true only when the suspend count is equal to zero.
|
||||
bool Resume();
|
||||
|
||||
// Call after opening channel, set channel and check whether the channel
|
||||
// needs to be suspended.
|
||||
void NotifyChannelOpened(nsIChannel* aChannel);
|
||||
|
||||
// Call before closing channel, reset the channel internal status if needed.
|
||||
void NotifyChannelClosing();
|
||||
|
||||
// Check whether we need to suspend the channel.
|
||||
void UpdateSuspendedStatusIfNeeded();
|
||||
// Tell the agent to manage the suspend status of the channel.
|
||||
void Delegate(nsIChannel* aChannel);
|
||||
// Stop the management of the suspend status of the channel.
|
||||
void Revoke();
|
||||
|
||||
private:
|
||||
// Only suspends channel but not changes the suspend count.
|
||||
void SuspendInternal();
|
||||
|
||||
nsIChannel* mChannel;
|
||||
nsIChannel* mChannel = nullptr;
|
||||
MediaCacheStream& mCacheStream;
|
||||
uint32_t mSuspendCount = 0;
|
||||
bool mIsChannelSuspended = false;
|
||||
@ -181,6 +175,7 @@ public:
|
||||
NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER
|
||||
|
||||
void Revoke();
|
||||
void SetReopenOnError() { mReopenOnError = true; }
|
||||
|
||||
private:
|
||||
Mutex mMutex;
|
||||
@ -188,6 +183,10 @@ public:
|
||||
// So it can be read without lock on the main thread or on other threads
|
||||
// with the lock.
|
||||
RefPtr<ChannelMediaResource> mResource;
|
||||
// When this flag is set, if we get a network error we should silently
|
||||
// reopen the stream. Main thread only.
|
||||
bool mReopenOnError = false;
|
||||
|
||||
const int64_t mOffset;
|
||||
const uint32_t mLoadID;
|
||||
};
|
||||
@ -201,7 +200,9 @@ protected:
|
||||
bool IsSuspendedByCache();
|
||||
// These are called on the main thread by Listener.
|
||||
nsresult OnStartRequest(nsIRequest* aRequest, int64_t aRequestOffset);
|
||||
nsresult OnStopRequest(nsIRequest* aRequest, nsresult aStatus);
|
||||
nsresult OnStopRequest(nsIRequest* aRequest,
|
||||
nsresult aStatus,
|
||||
bool aReopenOnError);
|
||||
nsresult OnDataAvailable(uint32_t aLoadID,
|
||||
nsIInputStream* aStream,
|
||||
uint32_t aCount);
|
||||
@ -255,9 +256,6 @@ protected:
|
||||
// Used by the cache to store the offset to seek to when we are resumed.
|
||||
// -1 means no seek initiated by the cache is waiting.
|
||||
int64_t mPendingSeekOffset = -1;
|
||||
// When this flag is set, if we get a network error we should silently
|
||||
// reopen the stream.
|
||||
bool mReopenOnError;
|
||||
|
||||
// Any thread access
|
||||
MediaCacheStream mCacheStream;
|
||||
|
@ -2517,8 +2517,6 @@ MediaCacheStream::Read(char* aBuffer, uint32_t aCount, uint32_t* aBytes)
|
||||
NS_ASSERTION(!NS_IsMainThread(), "Don't call on main thread");
|
||||
|
||||
ReentrantMonitorAutoEnter mon(mMediaCache->GetReentrantMonitor());
|
||||
if (mClosed)
|
||||
return NS_ERROR_FAILURE;
|
||||
|
||||
// Cache the offset in case it is changed again when we are waiting for the
|
||||
// monitor to be notified to avoid reading at the wrong position.
|
||||
@ -2527,10 +2525,16 @@ MediaCacheStream::Read(char* aBuffer, uint32_t aCount, uint32_t* aBytes)
|
||||
uint32_t count = 0;
|
||||
// Read one block (or part of a block) at a time
|
||||
while (count < aCount) {
|
||||
if (mClosed) {
|
||||
return NS_ERROR_ABORT;
|
||||
}
|
||||
|
||||
int32_t streamBlock = OffsetToBlockIndex(streamOffset);
|
||||
if (streamBlock < 0) {
|
||||
break;
|
||||
LOGE("Stream %p invalid offset=%" PRId64, this, streamOffset);
|
||||
return NS_ERROR_ILLEGAL_VALUE;
|
||||
}
|
||||
|
||||
uint32_t offsetInStreamBlock = uint32_t(streamOffset - streamBlock*BLOCK_SIZE);
|
||||
int64_t size = std::min<int64_t>(aCount - count, BLOCK_SIZE - offsetInStreamBlock);
|
||||
|
||||
@ -2551,12 +2555,6 @@ MediaCacheStream::Read(char* aBuffer, uint32_t aCount, uint32_t* aBytes)
|
||||
if (cacheBlock < 0) {
|
||||
// We don't have a complete cached block here.
|
||||
|
||||
if (count > 0) {
|
||||
// Some data has been read, so return what we've got instead of
|
||||
// blocking or trying to find a stream with a partial block.
|
||||
break;
|
||||
}
|
||||
|
||||
// See if the data is available in the partial cache block of any
|
||||
// stream reading this resource. We need to do this in case there is
|
||||
// another stream with this resource that has all the data to the end of
|
||||
@ -2566,7 +2564,8 @@ MediaCacheStream::Read(char* aBuffer, uint32_t aCount, uint32_t* aBytes)
|
||||
while (MediaCacheStream* stream = iter.Next()) {
|
||||
if (OffsetToBlockIndexUnchecked(stream->mChannelOffset) ==
|
||||
streamBlock &&
|
||||
streamOffset < stream->mChannelOffset) {
|
||||
streamOffset < stream->mChannelOffset &&
|
||||
stream->mChannelOffset == stream->mStreamLength) {
|
||||
streamWithPartialBlock = stream;
|
||||
break;
|
||||
}
|
||||
@ -2579,13 +2578,16 @@ MediaCacheStream::Read(char* aBuffer, uint32_t aCount, uint32_t* aBytes)
|
||||
// Clamp bytes until 64-bit file size issues are fixed.
|
||||
bytes = std::min(bytes, int64_t(INT32_MAX));
|
||||
MOZ_ASSERT(bytes >= 0 && bytes <= aCount, "Bytes out of range.");
|
||||
memcpy(aBuffer,
|
||||
streamWithPartialBlock->mPartialBlockBuffer.get() + offsetInStreamBlock, bytes);
|
||||
memcpy(aBuffer + count,
|
||||
streamWithPartialBlock->mPartialBlockBuffer.get() +
|
||||
offsetInStreamBlock,
|
||||
bytes);
|
||||
if (mCurrentMode == MODE_METADATA) {
|
||||
streamWithPartialBlock->mMetadataInPartialBlockBuffer = true;
|
||||
}
|
||||
streamOffset += bytes;
|
||||
count = bytes;
|
||||
count += bytes;
|
||||
// Break for we've reached EOS and have nothing more to read.
|
||||
break;
|
||||
}
|
||||
|
||||
@ -2598,11 +2600,6 @@ MediaCacheStream::Read(char* aBuffer, uint32_t aCount, uint32_t* aBytes)
|
||||
|
||||
// No data has been read yet, so block
|
||||
mon.Wait();
|
||||
if (mClosed) {
|
||||
// We may have successfully read some data, but let's just throw
|
||||
// that out.
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -2615,22 +2612,25 @@ MediaCacheStream::Read(char* aBuffer, uint32_t aCount, uint32_t* aBytes)
|
||||
nsresult rv = mMediaCache->ReadCacheFile(
|
||||
offset, aBuffer + count, int32_t(size), &bytes);
|
||||
if (NS_FAILED(rv)) {
|
||||
if (count == 0)
|
||||
return rv;
|
||||
// If we did successfully read some data, may as well return it
|
||||
break;
|
||||
nsCString name;
|
||||
GetErrorName(rv, name);
|
||||
LOGE("Stream %p ReadCacheFile failed, rv=%s", this, name.Data());
|
||||
return rv;
|
||||
}
|
||||
streamOffset += bytes;
|
||||
count += bytes;
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
// Some data was read, so queue an update since block priorities may
|
||||
// have changed
|
||||
mMediaCache->QueueUpdate();
|
||||
}
|
||||
LOG("Stream %p Read at %" PRId64 " count=%d", this, streamOffset-count, count);
|
||||
*aBytes = count;
|
||||
if (count == 0) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// Some data was read, so queue an update since block priorities may
|
||||
// have changed
|
||||
mMediaCache->QueueUpdate();
|
||||
|
||||
LOG("Stream %p Read at %" PRId64 " count=%d", this, streamOffset-count, count);
|
||||
mStreamOffset = streamOffset;
|
||||
return NS_OK;
|
||||
}
|
||||
|
@ -414,34 +414,14 @@ MediaResourceIndex::UncachedReadAt(int64_t aOffset,
|
||||
uint32_t aCount,
|
||||
uint32_t* aBytes) const
|
||||
{
|
||||
*aBytes = 0;
|
||||
if (aOffset < 0) {
|
||||
return NS_ERROR_ILLEGAL_VALUE;
|
||||
}
|
||||
if (aCount != 0) {
|
||||
for (;;) {
|
||||
uint32_t bytesRead = 0;
|
||||
nsresult rv = mResource->ReadAt(aOffset, aBuffer, aCount, &bytesRead);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
if (bytesRead == 0) {
|
||||
break;
|
||||
}
|
||||
*aBytes += bytesRead;
|
||||
aCount -= bytesRead;
|
||||
if (aCount == 0) {
|
||||
break;
|
||||
}
|
||||
aOffset += bytesRead;
|
||||
if (aOffset < 0) {
|
||||
// Very unlikely overflow.
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
aBuffer += bytesRead;
|
||||
}
|
||||
if (aCount == 0) {
|
||||
*aBytes = 0;
|
||||
return NS_OK;
|
||||
}
|
||||
return NS_OK;
|
||||
return mResource->ReadAt(aOffset, aBuffer, aCount, aBytes);
|
||||
}
|
||||
|
||||
nsresult
|
||||
@ -451,36 +431,15 @@ MediaResourceIndex::UncachedRangedReadAt(int64_t aOffset,
|
||||
uint32_t aExtraCount,
|
||||
uint32_t* aBytes) const
|
||||
{
|
||||
*aBytes = 0;
|
||||
uint32_t count = aRequestedCount + aExtraCount;
|
||||
if (aOffset < 0 || count < aRequestedCount) {
|
||||
return NS_ERROR_ILLEGAL_VALUE;
|
||||
}
|
||||
if (count != 0) {
|
||||
for (;;) {
|
||||
uint32_t bytesRead = 0;
|
||||
nsresult rv = mResource->ReadAt(aOffset, aBuffer, count, &bytesRead);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
if (bytesRead == 0) {
|
||||
break;
|
||||
}
|
||||
*aBytes += bytesRead;
|
||||
count -= bytesRead;
|
||||
if (count <= aExtraCount) {
|
||||
// We have read at least aRequestedCount, don't loop anymore.
|
||||
break;
|
||||
}
|
||||
aOffset += bytesRead;
|
||||
if (aOffset < 0) {
|
||||
// Very unlikely overflow.
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
aBuffer += bytesRead;
|
||||
}
|
||||
if (count == 0) {
|
||||
*aBytes = 0;
|
||||
return NS_OK;
|
||||
}
|
||||
return NS_OK;
|
||||
return mResource->ReadAt(aOffset, aBuffer, count, aBytes);
|
||||
}
|
||||
|
||||
nsresult
|
||||
@ -516,30 +475,17 @@ MediaResourceIndex::Seek(int32_t aWhence, int64_t aOffset)
|
||||
already_AddRefed<MediaByteBuffer>
|
||||
MediaResourceIndex::MediaReadAt(int64_t aOffset, uint32_t aCount) const
|
||||
{
|
||||
NS_ENSURE_TRUE(aOffset >= 0, nullptr);
|
||||
RefPtr<MediaByteBuffer> bytes = new MediaByteBuffer();
|
||||
if (aOffset < 0) {
|
||||
return bytes.forget();
|
||||
}
|
||||
bool ok = bytes->SetLength(aCount, fallible);
|
||||
NS_ENSURE_TRUE(ok, nullptr);
|
||||
char* curr = reinterpret_cast<char*>(bytes->Elements());
|
||||
const char* start = curr;
|
||||
while (aCount > 0) {
|
||||
uint32_t bytesRead;
|
||||
nsresult rv = mResource->ReadAt(aOffset, curr, aCount, &bytesRead);
|
||||
NS_ENSURE_SUCCESS(rv, nullptr);
|
||||
if (!bytesRead) {
|
||||
break;
|
||||
}
|
||||
aOffset += bytesRead;
|
||||
if (aOffset < 0) {
|
||||
// Very unlikely overflow.
|
||||
break;
|
||||
}
|
||||
aCount -= bytesRead;
|
||||
curr += bytesRead;
|
||||
}
|
||||
bytes->SetLength(curr - start);
|
||||
|
||||
uint32_t bytesRead = 0;
|
||||
nsresult rv = mResource->ReadAt(
|
||||
aOffset, reinterpret_cast<char*>(bytes->Elements()), aCount, &bytesRead);
|
||||
NS_ENSURE_SUCCESS(rv, nullptr);
|
||||
|
||||
bytes->SetLength(bytesRead);
|
||||
return bytes.forget();
|
||||
}
|
||||
|
||||
|
@ -268,10 +268,10 @@ GeckoMediaPluginService::GetCDM(const NodeId& aNodeId,
|
||||
}
|
||||
holder->Resolve(cdm, __func__);
|
||||
},
|
||||
[rawHolder] {
|
||||
[rawHolder](MediaResult result) {
|
||||
nsPrintfCString reason(
|
||||
"%s::%s failed since GetContentParent rejects the promise.",
|
||||
__CLASS__, __FUNCTION__);
|
||||
"%s::%s failed since GetContentParent rejects the promise with reason %s.",
|
||||
__CLASS__, __FUNCTION__, result.Description().get());
|
||||
UniquePtr<PromiseHolder> holder(rawHolder);
|
||||
holder->Reject(MediaResult(NS_ERROR_FAILURE, reason.get()), __func__);
|
||||
});
|
||||
|
@ -53,7 +53,7 @@ struct NodeId
|
||||
};
|
||||
|
||||
typedef MozPromise<RefPtr<GMPContentParent::CloseBlocker>,
|
||||
nsresult,
|
||||
MediaResult,
|
||||
/* IsExclusive = */ true>
|
||||
GetGMPContentParentPromise;
|
||||
typedef MozPromise<RefPtr<ChromiumCDMParent>,
|
||||
|
@ -84,6 +84,8 @@ GeckoMediaPluginServiceChild::GetContentParent(GMPCrashHelper* aHelper,
|
||||
nsCString displayName;
|
||||
uint32_t pluginId = 0;
|
||||
ipc::Endpoint<PGMPContentParent> endpoint;
|
||||
nsCString errorDescription = NS_LITERAL_CSTRING("");
|
||||
|
||||
bool ok = child->SendLaunchGMP(nodeIdString,
|
||||
api,
|
||||
tags,
|
||||
@ -92,7 +94,8 @@ GeckoMediaPluginServiceChild::GetContentParent(GMPCrashHelper* aHelper,
|
||||
&otherProcess,
|
||||
&displayName,
|
||||
&endpoint,
|
||||
&rv);
|
||||
&rv,
|
||||
&errorDescription);
|
||||
if (helper && pluginId) {
|
||||
// Note: Even if the launch failed, we need to connect the crash
|
||||
// helper so that if the launch failed due to the plugin crashing,
|
||||
@ -103,10 +106,14 @@ GeckoMediaPluginServiceChild::GetContentParent(GMPCrashHelper* aHelper,
|
||||
}
|
||||
|
||||
if (!ok || NS_FAILED(rv)) {
|
||||
LOGD(("GeckoMediaPluginServiceChild::GetContentParent SendLaunchGMP "
|
||||
"failed rv=0x%x",
|
||||
static_cast<uint32_t>(rv)));
|
||||
holder->Reject(rv, __func__);
|
||||
MediaResult error(
|
||||
rv,
|
||||
nsPrintfCString("GeckoMediaPluginServiceChild::GetContentParent "
|
||||
"SendLaunchGMPForNodeId failed with description (%s)",
|
||||
errorDescription.get()));
|
||||
|
||||
LOGD(("%s", error.Description().get()));
|
||||
holder->Reject(error, __func__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -120,9 +127,9 @@ GeckoMediaPluginServiceChild::GetContentParent(GMPCrashHelper* aHelper,
|
||||
new GMPContentParent::CloseBlocker(parent));
|
||||
holder->Resolve(blocker, __func__);
|
||||
},
|
||||
[rawHolder](nsresult rv) {
|
||||
[rawHolder](MediaResult result) {
|
||||
UniquePtr<MozPromiseHolder<GetGMPContentParentPromise>> holder(rawHolder);
|
||||
holder->Reject(rv, __func__);
|
||||
holder->Reject(result, __func__);
|
||||
});
|
||||
|
||||
return promise;
|
||||
@ -146,7 +153,9 @@ GeckoMediaPluginServiceChild::GetContentParent(GMPCrashHelper* aHelper,
|
||||
nsTArray<nsCString> tags(aTags);
|
||||
RefPtr<GMPCrashHelper> helper(aHelper);
|
||||
RefPtr<GeckoMediaPluginServiceChild> self(this);
|
||||
GetServiceChild()->Then(thread, __func__,
|
||||
GetServiceChild()->Then(
|
||||
thread,
|
||||
__func__,
|
||||
[self, nodeId, api, tags, helper, rawHolder](GMPServiceChild* child) {
|
||||
UniquePtr<MozPromiseHolder<GetGMPContentParentPromise>> holder(rawHolder);
|
||||
nsresult rv;
|
||||
@ -158,6 +167,7 @@ GeckoMediaPluginServiceChild::GetContentParent(GMPCrashHelper* aHelper,
|
||||
nsCString displayName;
|
||||
uint32_t pluginId = 0;
|
||||
ipc::Endpoint<PGMPContentParent> endpoint;
|
||||
nsCString errorDescription = NS_LITERAL_CSTRING("");
|
||||
|
||||
bool ok = child->SendLaunchGMPForNodeId(nodeId,
|
||||
api,
|
||||
@ -167,7 +177,8 @@ GeckoMediaPluginServiceChild::GetContentParent(GMPCrashHelper* aHelper,
|
||||
&otherProcess,
|
||||
&displayName,
|
||||
&endpoint,
|
||||
&rv);
|
||||
&rv,
|
||||
&errorDescription);
|
||||
|
||||
if (helper && pluginId) {
|
||||
// Note: Even if the launch failed, we need to connect the crash
|
||||
@ -179,9 +190,14 @@ GeckoMediaPluginServiceChild::GetContentParent(GMPCrashHelper* aHelper,
|
||||
}
|
||||
|
||||
if (!ok || NS_FAILED(rv)) {
|
||||
LOGD(("GeckoMediaPluginServiceChild::GetContentParent SendLaunchGMP failed rv=%" PRIu32,
|
||||
static_cast<uint32_t>(rv)));
|
||||
holder->Reject(rv, __func__);
|
||||
MediaResult error(
|
||||
rv,
|
||||
nsPrintfCString("GeckoMediaPluginServiceChild::GetContentParent "
|
||||
"SendLaunchGMPForNodeId failed with description (%s)",
|
||||
errorDescription.get()));
|
||||
|
||||
LOGD(("%s", error.Description().get()));
|
||||
holder->Reject(error, __func__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -195,9 +211,9 @@ GeckoMediaPluginServiceChild::GetContentParent(GMPCrashHelper* aHelper,
|
||||
RefPtr<GMPContentParent::CloseBlocker> blocker(new GMPContentParent::CloseBlocker(parent));
|
||||
holder->Resolve(blocker, __func__);
|
||||
},
|
||||
[rawHolder](nsresult rv) {
|
||||
[rawHolder](MediaResult result) {
|
||||
UniquePtr<MozPromiseHolder<GetGMPContentParentPromise>> holder(rawHolder);
|
||||
holder->Reject(rv, __func__);
|
||||
holder->Reject(result, __func__);
|
||||
});
|
||||
|
||||
return promise;
|
||||
|
@ -7,6 +7,7 @@
|
||||
#define GMPServiceChild_h_
|
||||
|
||||
#include "GMPService.h"
|
||||
#include "MediaResult.h"
|
||||
#include "base/process.h"
|
||||
#include "mozilla/ipc/Transport.h"
|
||||
#include "mozilla/gmp/PGMPServiceChild.h"
|
||||
@ -65,7 +66,8 @@ protected:
|
||||
private:
|
||||
friend class OpenPGMPServiceChild;
|
||||
|
||||
typedef MozPromise<GMPServiceChild*, nsresult, /* IsExclusive = */ true> GetServiceChildPromise;
|
||||
typedef MozPromise<GMPServiceChild*, MediaResult, /* IsExclusive = */ true>
|
||||
GetServiceChildPromise;
|
||||
RefPtr<GetServiceChildPromise> GetServiceChild();
|
||||
|
||||
nsTArray<MozPromiseHolder<GetServiceChildPromise>> mGetServiceChildPromises;
|
||||
|
@ -1735,10 +1735,12 @@ GMPServiceParent::RecvLaunchGMP(const nsCString& aNodeId,
|
||||
ProcessId* aOutProcessId,
|
||||
nsCString* aOutDisplayName,
|
||||
Endpoint<PGMPContentParent>* aOutEndpoint,
|
||||
nsresult* aOutRv)
|
||||
nsresult* aOutRv,
|
||||
nsCString* aOutErrorDescription)
|
||||
{
|
||||
if (mService->IsShuttingDown()) {
|
||||
*aOutRv = NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
|
||||
*aOutErrorDescription = NS_LITERAL_CSTRING("Service is shutting down.");
|
||||
return IPC_OK();
|
||||
}
|
||||
|
||||
@ -1747,12 +1749,14 @@ GMPServiceParent::RecvLaunchGMP(const nsCString& aNodeId,
|
||||
*aOutPluginId = gmp->GetPluginId();
|
||||
} else {
|
||||
*aOutRv = NS_ERROR_FAILURE;
|
||||
*aOutErrorDescription = NS_LITERAL_CSTRING("SelectPluginForAPI returns nullptr.");
|
||||
*aOutPluginId = 0;
|
||||
return IPC_OK();
|
||||
}
|
||||
|
||||
if (!gmp->EnsureProcessLoaded(aOutProcessId)) {
|
||||
*aOutRv = NS_ERROR_FAILURE;
|
||||
*aOutErrorDescription = NS_LITERAL_CSTRING("Process has not loaded.");
|
||||
return IPC_OK();
|
||||
}
|
||||
|
||||
@ -1765,9 +1769,12 @@ GMPServiceParent::RecvLaunchGMP(const nsCString& aNodeId,
|
||||
|
||||
Endpoint<PGMPContentParent> parent;
|
||||
Endpoint<PGMPContentChild> child;
|
||||
if (NS_WARN_IF(NS_FAILED(PGMPContent::CreateEndpoints(
|
||||
OtherPid(), *aOutProcessId, &parent, &child)))) {
|
||||
*aOutRv = NS_ERROR_FAILURE;
|
||||
nsresult rv =
|
||||
PGMPContent::CreateEndpoints(OtherPid(), *aOutProcessId, &parent, &child);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
*aOutRv = rv;
|
||||
*aOutErrorDescription =
|
||||
NS_LITERAL_CSTRING("PGMPContent::CreateEndpoints failed.");
|
||||
return IPC_OK();
|
||||
}
|
||||
|
||||
@ -1775,6 +1782,8 @@ GMPServiceParent::RecvLaunchGMP(const nsCString& aNodeId,
|
||||
|
||||
if (!gmp->SendInitGMPContentChild(Move(child))) {
|
||||
*aOutRv = NS_ERROR_FAILURE;
|
||||
*aOutErrorDescription =
|
||||
NS_LITERAL_CSTRING("SendInitGMPContentChild failed.");
|
||||
return IPC_OK();
|
||||
}
|
||||
|
||||
@ -1794,13 +1803,15 @@ GMPServiceParent::RecvLaunchGMPForNodeId(
|
||||
ProcessId* aOutId,
|
||||
nsCString* aOutDisplayName,
|
||||
Endpoint<PGMPContentParent>* aOutEndpoint,
|
||||
nsresult* aOutRv)
|
||||
nsresult* aOutRv,
|
||||
nsCString* aOutErrorDescription)
|
||||
{
|
||||
nsCString nodeId;
|
||||
nsresult rv = mService->GetNodeId(
|
||||
aNodeId.mOrigin(), aNodeId.mTopLevelOrigin(), aNodeId.mGMPName(), nodeId);
|
||||
if (!NS_SUCCEEDED(rv)) {
|
||||
*aOutRv = rv;
|
||||
*aOutErrorDescription = NS_LITERAL_CSTRING("GetNodeId failed.");
|
||||
return IPC_OK();
|
||||
}
|
||||
return RecvLaunchGMP(nodeId,
|
||||
@ -1811,7 +1822,8 @@ GMPServiceParent::RecvLaunchGMPForNodeId(
|
||||
aOutId,
|
||||
aOutDisplayName,
|
||||
aOutEndpoint,
|
||||
aOutRv);
|
||||
aOutRv,
|
||||
aOutErrorDescription);
|
||||
}
|
||||
|
||||
mozilla::ipc::IPCResult
|
||||
|
@ -246,7 +246,8 @@ public:
|
||||
ProcessId* aOutID,
|
||||
nsCString* aOutDisplayName,
|
||||
Endpoint<PGMPContentParent>* aOutEndpoint,
|
||||
nsresult* aOutRv) override;
|
||||
nsresult* aOutRv,
|
||||
nsCString* aOutErrorDescription) override;
|
||||
|
||||
ipc::IPCResult RecvLaunchGMPForNodeId(
|
||||
const NodeIdData& nodeId,
|
||||
@ -257,7 +258,8 @@ public:
|
||||
ProcessId* aOutID,
|
||||
nsCString* aOutDisplayName,
|
||||
Endpoint<PGMPContentParent>* aOutEndpoint,
|
||||
nsresult* aOutRv) override;
|
||||
nsresult* aOutRv,
|
||||
nsCString* aOutErrorDescription) override;
|
||||
|
||||
private:
|
||||
void CloseTransport(Monitor* aSyncMonitor, bool* aCompleted);
|
||||
|
@ -22,7 +22,8 @@ parent:
|
||||
ProcessId id,
|
||||
nsCString displayName,
|
||||
Endpoint<PGMPContentParent> endpoint,
|
||||
nsresult aResult);
|
||||
nsresult aResult,
|
||||
nsCString aErrorDescription);
|
||||
|
||||
sync LaunchGMPForNodeId(NodeIdData nodeId,
|
||||
nsCString api,
|
||||
@ -32,7 +33,8 @@ parent:
|
||||
ProcessId id,
|
||||
nsCString displayName,
|
||||
Endpoint<PGMPContentParent> endpoint,
|
||||
nsresult aResult);
|
||||
nsresult aResult,
|
||||
nsCString aErrorDescription);
|
||||
|
||||
sync GetGMPNodeId(nsString origin, nsString topLevelOrigin, nsString gmpName)
|
||||
returns (nsCString id);
|
||||
|
@ -305,8 +305,14 @@ const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
|
||||
rt: "ruby"
|
||||
};
|
||||
|
||||
const PARSE_CONTENT_MODE = {
|
||||
NORMAL_CUE: "normal_cue",
|
||||
PSUEDO_CUE: "pseudo_cue",
|
||||
DOCUMENT_FRAGMENT: "document_fragment",
|
||||
REGION_CUE: "region_cue",
|
||||
}
|
||||
// Parse content into a document fragment.
|
||||
function parseContent(window, input, bReturnFrag) {
|
||||
function parseContent(window, input, mode) {
|
||||
function nextToken() {
|
||||
// Check for end-of-string.
|
||||
if (!input) {
|
||||
@ -384,16 +390,20 @@ const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
|
||||
return hours + ':' + minutes + ':' + seconds + '.' + f;
|
||||
}
|
||||
|
||||
var isFirefoxSupportPseudo = (/firefox/i.test(window.navigator.userAgent))
|
||||
&& Services.prefs.getBoolPref("media.webvtt.pseudo.enabled");
|
||||
var root;
|
||||
if (bReturnFrag) {
|
||||
root = window.document.createDocumentFragment();
|
||||
} else if (isFirefoxSupportPseudo) {
|
||||
root = window.document.createElement("div", {pseudo: "::cue"});
|
||||
} else {
|
||||
root = window.document.createElement("div");
|
||||
switch (mode) {
|
||||
case PARSE_CONTENT_MODE.PSUEDO_CUE:
|
||||
root = window.document.createElement("div", {pseudo: "::cue"});
|
||||
break;
|
||||
case PARSE_CONTENT_MODE.NORMAL_CUE:
|
||||
case PARSE_CONTENT_MODE.REGION_CUE:
|
||||
root = window.document.createElement("div");
|
||||
break;
|
||||
case PARSE_CONTENT_MODE.DOCUMENT_FRAGMENT:
|
||||
root = window.document.createDocumentFragment();
|
||||
break;
|
||||
}
|
||||
|
||||
var current = root,
|
||||
t,
|
||||
tagStack = [];
|
||||
@ -494,7 +504,11 @@ const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
// Parse our cue's text into a DOM tree rooted at 'cueDiv'. This div will
|
||||
// have inline positioning and will function as the cue background box.
|
||||
this.cueDiv = parseContent(window, cue.text, false);
|
||||
if (isFirefoxSupportPseudo) {
|
||||
this.cueDiv = parseContent(window, cue.text, PARSE_CONTENT_MODE.PSUEDO_CUE);
|
||||
} else {
|
||||
this.cueDiv = parseContent(window, cue.text, PARSE_CONTENT_MODE.NORMAL_CUE);
|
||||
}
|
||||
var styles = {
|
||||
color: color,
|
||||
backgroundColor: backgroundColor,
|
||||
@ -586,6 +600,79 @@ const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
|
||||
CueStyleBox.prototype = _objCreate(StyleBox.prototype);
|
||||
CueStyleBox.prototype.constructor = CueStyleBox;
|
||||
|
||||
function RegionNodeBox(window, region, container) {
|
||||
StyleBox.call(this);
|
||||
|
||||
var boxLineHeight = container.height * 0.0533 // 0.0533vh ? 5.33vh
|
||||
var boxHeight = boxLineHeight * region.lines;
|
||||
var boxWidth = container.width * region.width / 100; // convert percentage to px
|
||||
|
||||
var regionNodeStyles = {
|
||||
position: "absolute",
|
||||
height: boxHeight + "px",
|
||||
width: boxWidth + "px",
|
||||
top: (region.viewportAnchorY * container.height / 100) - (region.regionAnchorY * boxHeight / 100) + "px",
|
||||
left: (region.viewportAnchorX * container.width / 100) - (region.regionAnchorX * boxWidth / 100) + "px",
|
||||
lineHeight: boxLineHeight + "px",
|
||||
writingMode: "horizontal-tb",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||||
wordWrap: "break-word",
|
||||
overflowWrap: "break-word",
|
||||
font: (boxLineHeight/1.3) + "px sans-serif",
|
||||
color: "rgba(255, 255, 255, 1)",
|
||||
overflow: "hidden",
|
||||
minHeight: "0px",
|
||||
maxHeight: boxHeight + "px",
|
||||
display: "inline-flex",
|
||||
flexFlow: "column",
|
||||
justifyContent: "flex-end",
|
||||
};
|
||||
|
||||
this.div = window.document.createElement("div");
|
||||
this.div.id = region.id; // useless?
|
||||
this.applyStyles(regionNodeStyles);
|
||||
}
|
||||
RegionNodeBox.prototype = _objCreate(StyleBox.prototype);
|
||||
RegionNodeBox.prototype.constructor = RegionNodeBox;
|
||||
|
||||
function RegionCueStyleBox(window, cue) {
|
||||
StyleBox.call(this);
|
||||
this.cueDiv = parseContent(window, cue.text, PARSE_CONTENT_MODE.REGION_CUE);
|
||||
|
||||
var regionCueStyles = {
|
||||
position: "relative",
|
||||
writingMode: "horizontal-tb",
|
||||
unicodeBidi: "plaintext",
|
||||
width: "auto",
|
||||
height: "auto",
|
||||
textAlign: cue.align,
|
||||
};
|
||||
// TODO: fix me, LTR and RTL ? using margin replace the "left/right"
|
||||
// 6.1.14.3.3
|
||||
var offset = cue.computedPosition * cue.region.width / 100;
|
||||
// 6.1.14.3.4
|
||||
switch (cue.align) {
|
||||
case "start":
|
||||
case "left":
|
||||
regionCueStyles.left = offset + "%";
|
||||
regionCueStyles.right = "auto";
|
||||
break;
|
||||
case "end":
|
||||
case "right":
|
||||
regionCueStyles.left = "auto";
|
||||
regionCueStyles.right = offset + "%";
|
||||
break;
|
||||
case "middle":
|
||||
break;
|
||||
}
|
||||
|
||||
this.div = window.document.createElement("div");
|
||||
this.applyStyles(regionCueStyles);
|
||||
this.div.appendChild(this.cueDiv);
|
||||
}
|
||||
RegionCueStyleBox.prototype = _objCreate(StyleBox.prototype);
|
||||
RegionCueStyleBox.prototype.constructor = RegionCueStyleBox;
|
||||
|
||||
// Represents the co-ordinates of an Element in a way that we can easily
|
||||
// compute things with such as if it overlaps or intersects with another Element.
|
||||
// Can initialize it with either a StyleBox or another BoxPosition.
|
||||
@ -896,7 +983,7 @@ const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
|
||||
if (!window) {
|
||||
return null;
|
||||
}
|
||||
return parseContent(window, cuetext, true);
|
||||
return parseContent(window, cuetext, PARSE_CONTENT_MODE.DOCUMENT_FRAGMENT);
|
||||
};
|
||||
|
||||
var FONT_SIZE_PERCENT = 0.05;
|
||||
@ -962,29 +1049,66 @@ const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
|
||||
};
|
||||
|
||||
(function() {
|
||||
var styleBox, cue;
|
||||
var styleBox, cue, controlBarBox;
|
||||
|
||||
if (controlBarShown) {
|
||||
controlBarBox = BoxPosition.getSimpleBoxPosition(controlBar);
|
||||
// Add an empty output box that cover the same region as video control bar.
|
||||
boxPositions.push(BoxPosition.getSimpleBoxPosition(controlBar));
|
||||
boxPositions.push(controlBarBox);
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webvtt/#processing-model 6.1.12.1
|
||||
// Create regionNode
|
||||
var regionNodeBoxes = {};
|
||||
var regionNodeBox;
|
||||
|
||||
for (var i = 0; i < cues.length; i++) {
|
||||
cue = cues[i];
|
||||
if (cue.region != null) {
|
||||
// 6.1.14.1
|
||||
styleBox = new RegionCueStyleBox(window, cue);
|
||||
|
||||
// Compute the intial position and styles of the cue div.
|
||||
styleBox = new CueStyleBox(window, cue, styleOptions);
|
||||
styleBox.cueDiv.style.setProperty("--cue-font-size", fontSize + "px");
|
||||
rootOfCues.appendChild(styleBox.div);
|
||||
if (!regionNodeBoxes[cue.region.id]) {
|
||||
// create regionNode
|
||||
// Adjust the container hieght to exclude the controlBar
|
||||
var adjustContainerBox = BoxPosition.getSimpleBoxPosition(rootOfCues);
|
||||
if (controlBarShown) {
|
||||
adjustContainerBox.height -= controlBarBox.height;
|
||||
adjustContainerBox.bottom += controlBarBox.height;
|
||||
}
|
||||
regionNodeBox = new RegionNodeBox(window, cue.region, adjustContainerBox);
|
||||
regionNodeBoxes[cue.region.id] = regionNodeBox;
|
||||
}
|
||||
// 6.1.14.3
|
||||
var currentRegionBox = regionNodeBoxes[cue.region.id];
|
||||
var currentRegionNodeDiv = currentRegionBox.div;
|
||||
// 6.1.14.3.2
|
||||
// TODO: fix me, it looks like the we need to set/change "top" attribute at the styleBox.div
|
||||
// to do the "scroll up", however, we do not implement it yet?
|
||||
if (cue.region.scroll == "up" && currentRegionNodeDiv.childElementCount > 0) {
|
||||
styleBox.div.style.transitionProperty = "top";
|
||||
styleBox.div.style.transitionDuration = "0.433s";
|
||||
}
|
||||
|
||||
// Move the cue div to it's correct line position.
|
||||
moveBoxToLinePosition(window, styleBox, containerBox, boxPositions);
|
||||
currentRegionNodeDiv.appendChild(styleBox.div);
|
||||
rootOfCues.appendChild(currentRegionNodeDiv);
|
||||
cue.displayState = styleBox.div;
|
||||
boxPositions.push(BoxPosition.getSimpleBoxPosition(currentRegionBox));
|
||||
} else {
|
||||
// Compute the intial position and styles of the cue div.
|
||||
styleBox = new CueStyleBox(window, cue, styleOptions);
|
||||
styleBox.cueDiv.style.setProperty("--cue-font-size", fontSize + "px");
|
||||
rootOfCues.appendChild(styleBox.div);
|
||||
|
||||
// Remember the computed div so that we don't have to recompute it later
|
||||
// if we don't have too.
|
||||
cue.displayState = styleBox.div;
|
||||
// Move the cue div to it's correct line position.
|
||||
moveBoxToLinePosition(window, styleBox, containerBox, boxPositions);
|
||||
|
||||
boxPositions.push(BoxPosition.getSimpleBoxPosition(styleBox));
|
||||
// Remember the computed div so that we don't have to recompute it later
|
||||
// if we don't have too.
|
||||
cue.displayState = styleBox.div;
|
||||
|
||||
boxPositions.push(BoxPosition.getSimpleBoxPosition(styleBox));
|
||||
}
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
@ -4,9 +4,7 @@ default-preferences pref(dom.vr.puppet.enabled,true) pref(dom.vr.test.enabled,tr
|
||||
|
||||
# VR SubmitFrame is only implemented for D3D11.1 and MacOSX now.
|
||||
# Our Windows 7 test machines don't support D3D11.1, so we run these tests on Windows 8+ only.
|
||||
# We need to continue to investigate why these reftests can be run well in local,
|
||||
# but will be suspended until terminating on reftest D3D11 debug build.
|
||||
skip-if((!winWidget&&release_or_beta)||Android||gtkWidget||(winWidget&&isDebugBuild)||!layersGPUAccelerated||/^Windows\x20NT\x206\.1/.test(http.oscpu)) == draw_rect.html wrapper.html?draw_rect.png
|
||||
skip-if((!winWidget&&release_or_beta)||Android||gtkWidget||!layersGPUAccelerated||/^Windows\x20NT\x206\.1/.test(http.oscpu)) == draw_rect.html wrapper.html?draw_rect.png
|
||||
# On MacOSX platform, getting different color interpolation result.
|
||||
# For lower resolution Mac hardware, we need to adjust it to fuzzy-if(cocoaWidget,1,1200).
|
||||
fuzzy-if(cocoaWidget,1,600) skip-if((!winWidget&&release_or_beta)||Android||gtkWidget||(winWidget&&isDebugBuild)||!layersGPUAccelerated||/^Windows\x20NT\x206\.1/.test(http.oscpu)) == change_size.html wrapper.html?change_size.png
|
||||
fuzzy-if(cocoaWidget,1,600) skip-if((!winWidget&&release_or_beta)||Android||gtkWidget||!layersGPUAccelerated||/^Windows\x20NT\x206\.1/.test(http.oscpu)) == change_size.html wrapper.html?change_size.png
|
||||
|
@ -2384,23 +2384,18 @@ EditorBase::ScrollSelectionIntoView(bool aScrollToAnchor)
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
void
|
||||
EditorBase::FindBetterInsertionPoint(nsCOMPtr<nsIDOMNode>& aNode,
|
||||
int32_t& aOffset)
|
||||
EditorRawDOMPoint
|
||||
EditorBase::FindBetterInsertionPoint(const EditorRawDOMPoint& aPoint)
|
||||
{
|
||||
nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
|
||||
FindBetterInsertionPoint(node, aOffset, nullptr);
|
||||
aNode = do_QueryInterface(node);
|
||||
}
|
||||
if (NS_WARN_IF(!aPoint.IsSet())) {
|
||||
return aPoint;
|
||||
}
|
||||
|
||||
void
|
||||
EditorBase::FindBetterInsertionPoint(nsCOMPtr<nsINode>& aNode,
|
||||
int32_t& aOffset,
|
||||
nsCOMPtr<nsIContent>* aSelChild)
|
||||
{
|
||||
if (aNode->IsNodeOfType(nsINode::eTEXT)) {
|
||||
MOZ_ASSERT(aPoint.IsSetAndValid());
|
||||
|
||||
if (aPoint.Container()->IsNodeOfType(nsINode::eTEXT)) {
|
||||
// There is no "better" insertion point.
|
||||
return;
|
||||
return aPoint;
|
||||
}
|
||||
|
||||
if (!IsPlaintextEditor()) {
|
||||
@ -2408,57 +2403,44 @@ EditorBase::FindBetterInsertionPoint(nsCOMPtr<nsINode>& aNode,
|
||||
// WARNING: When you add some code to find better node in HTML editor,
|
||||
// you need to call this before calling InsertTextImpl() in
|
||||
// HTMLEditRules.
|
||||
return;
|
||||
return aPoint;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsINode> node = aNode;
|
||||
int32_t offset = aOffset;
|
||||
|
||||
nsCOMPtr<nsINode> root = GetRoot();
|
||||
if (aNode == root) {
|
||||
if (aPoint.Container() == root) {
|
||||
// In some cases, aNode is the anonymous DIV, and offset is 0. To avoid
|
||||
// injecting unneeded text nodes, we first look to see if we have one
|
||||
// available. In that case, we'll just adjust node and offset accordingly.
|
||||
if (!offset && node->HasChildren() &&
|
||||
node->GetFirstChild()->IsNodeOfType(nsINode::eTEXT)) {
|
||||
aNode = node->GetFirstChild();
|
||||
aOffset = 0;
|
||||
if (aSelChild) {
|
||||
*aSelChild = nullptr;
|
||||
}
|
||||
return;
|
||||
if (aPoint.IsStartOfContainer() &&
|
||||
aPoint.Container()->HasChildren() &&
|
||||
aPoint.Container()->GetFirstChild()->IsNodeOfType(nsINode::eTEXT)) {
|
||||
return EditorRawDOMPoint(aPoint.Container()->GetFirstChild(), 0);
|
||||
}
|
||||
|
||||
// In some other cases, aNode is the anonymous DIV, and offset points to the
|
||||
// terminating mozBR. In that case, we'll adjust aInOutNode and
|
||||
// aInOutOffset to the preceding text node, if any.
|
||||
if (offset) {
|
||||
if (!aPoint.IsStartOfContainer()) {
|
||||
if (AsHTMLEditor()) {
|
||||
// Fall back to a slow path that uses GetChildAt() for Thunderbird's
|
||||
// plaintext editor.
|
||||
nsIContent* child = node->GetChildAt(offset - 1);
|
||||
nsIContent* child = aPoint.GetPreviousSiblingOfChildAtOffset();
|
||||
if (child && child->IsNodeOfType(nsINode::eTEXT)) {
|
||||
NS_ENSURE_TRUE_VOID(node->Length() <= INT32_MAX);
|
||||
aNode = child;
|
||||
aOffset = static_cast<int32_t>(aNode->Length());
|
||||
if (aSelChild) {
|
||||
*aSelChild = nullptr;
|
||||
if (NS_WARN_IF(child->Length() > INT32_MAX)) {
|
||||
return aPoint;
|
||||
}
|
||||
return;
|
||||
return EditorRawDOMPoint(child, child->Length());
|
||||
}
|
||||
} else {
|
||||
// If we're in a real plaintext editor, use a fast path that avoids
|
||||
// calling GetChildAt() which may perform a linear search.
|
||||
nsIContent* child = node->GetLastChild();
|
||||
nsIContent* child = aPoint.Container()->GetLastChild();
|
||||
while (child) {
|
||||
if (child->IsNodeOfType(nsINode::eTEXT)) {
|
||||
NS_ENSURE_TRUE_VOID(node->Length() <= INT32_MAX);
|
||||
aNode = child;
|
||||
aOffset = static_cast<int32_t>(aNode->Length());
|
||||
if (aSelChild) {
|
||||
*aSelChild = nullptr;
|
||||
if (NS_WARN_IF(child->Length() > INT32_MAX)) {
|
||||
return aPoint;
|
||||
}
|
||||
return;
|
||||
return EditorRawDOMPoint(child, child->Length());
|
||||
}
|
||||
child = child->GetPreviousSibling();
|
||||
}
|
||||
@ -2469,125 +2451,130 @@ EditorBase::FindBetterInsertionPoint(nsCOMPtr<nsINode>& aNode,
|
||||
// Sometimes, aNode is the mozBR element itself. In that case, we'll adjust
|
||||
// the insertion point to the previous text node, if one exists, or to the
|
||||
// parent anonymous DIV.
|
||||
if (TextEditUtils::IsMozBR(node) && !offset) {
|
||||
if (node->GetPreviousSibling() &&
|
||||
node->GetPreviousSibling()->IsNodeOfType(nsINode::eTEXT)) {
|
||||
NS_ENSURE_TRUE_VOID(node->Length() <= INT32_MAX);
|
||||
aNode = node->GetPreviousSibling();
|
||||
aOffset = static_cast<int32_t>(aNode->Length());
|
||||
if (aSelChild) {
|
||||
*aSelChild = nullptr;
|
||||
if (TextEditUtils::IsMozBR(aPoint.Container()) &&
|
||||
aPoint.IsStartOfContainer()) {
|
||||
nsIContent* previousSibling = aPoint.Container()->GetPreviousSibling();
|
||||
if (previousSibling && previousSibling->IsNodeOfType(nsINode::eTEXT)) {
|
||||
if (NS_WARN_IF(previousSibling->Length() > INT32_MAX)) {
|
||||
return aPoint;
|
||||
}
|
||||
return;
|
||||
return EditorRawDOMPoint(previousSibling, previousSibling->Length());
|
||||
}
|
||||
|
||||
if (node->GetParentNode() && node->GetParentNode() == root) {
|
||||
if (aSelChild) {
|
||||
*aSelChild = node->AsContent();
|
||||
}
|
||||
aNode = node->GetParentNode();
|
||||
aOffset = 0;
|
||||
return;
|
||||
nsINode* parentOfContainer = aPoint.Container()->GetParentNode();
|
||||
if (parentOfContainer && parentOfContainer == root) {
|
||||
return EditorRawDOMPoint(parentOfContainer,
|
||||
aPoint.Container()->AsContent(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
return aPoint;
|
||||
}
|
||||
|
||||
nsresult
|
||||
EditorBase::InsertTextImpl(const nsAString& aStringToInsert,
|
||||
nsCOMPtr<nsINode>* aInOutNode,
|
||||
nsCOMPtr<nsIContent>* aInOutChildAtOffset,
|
||||
int32_t* aInOutOffset,
|
||||
nsIDocument* aDoc)
|
||||
EditorBase::InsertTextImpl(nsIDocument& aDocument,
|
||||
const nsAString& aStringToInsert,
|
||||
const EditorRawDOMPoint& aPointToInsert,
|
||||
EditorRawDOMPoint* aPointAfterInsertedString)
|
||||
{
|
||||
// NOTE: caller *must* have already used AutoTransactionsConserveSelection
|
||||
// stack-based class to turn off txn selection updating. Caller also turned
|
||||
// on rules sniffing if desired.
|
||||
|
||||
NS_ENSURE_TRUE(aInOutNode && *aInOutNode && aInOutOffset && aDoc,
|
||||
NS_ERROR_NULL_POINTER);
|
||||
if (NS_WARN_IF(!aPointToInsert.IsSet())) {
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
MOZ_ASSERT(aPointToInsert.IsSetAndValid());
|
||||
|
||||
if (!ShouldHandleIMEComposition() && aStringToInsert.IsEmpty()) {
|
||||
if (aPointAfterInsertedString) {
|
||||
*aPointAfterInsertedString = aPointToInsert;
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// This method doesn't support over INT32_MAX length text since aInOutOffset
|
||||
// is int32_t*.
|
||||
CheckedInt<int32_t> lengthToInsert(aStringToInsert.Length());
|
||||
NS_ENSURE_TRUE(lengthToInsert.isValid(), NS_ERROR_INVALID_ARG);
|
||||
|
||||
nsCOMPtr<nsINode> node = *aInOutNode;
|
||||
int32_t offset = *aInOutOffset;
|
||||
nsCOMPtr<nsIContent> child = *aInOutChildAtOffset;
|
||||
|
||||
MOZ_ASSERT(!node->IsContainerNode() ||
|
||||
node->Length() == static_cast<uint32_t>(offset) ||
|
||||
node->GetChildAt(offset) == *aInOutChildAtOffset,
|
||||
"|child| must be a child node at |offset| in |node| unless it's a text "
|
||||
"or some other data node, or after the last child");
|
||||
if (NS_WARN_IF(!lengthToInsert.isValid())) {
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
// In some cases, the node may be the anonymous div elemnt or a mozBR
|
||||
// element. Let's try to look for better insertion point in the nearest
|
||||
// text node if there is.
|
||||
FindBetterInsertionPoint(node, offset, address_of(child));
|
||||
EditorRawDOMPoint pointToInsert = FindBetterInsertionPoint(aPointToInsert);
|
||||
|
||||
// If a neighboring text node already exists, use that
|
||||
if (!node->IsNodeOfType(nsINode::eTEXT)) {
|
||||
if (offset && child && child->GetPreviousSibling() &&
|
||||
child->GetPreviousSibling()->IsNodeOfType(nsINode::eTEXT)) {
|
||||
node = child->GetPreviousSibling();
|
||||
offset = node->Length();
|
||||
} else if (offset < static_cast<int32_t>(node->Length()) &&
|
||||
child && child->IsNodeOfType(nsINode::eTEXT)) {
|
||||
node = child;
|
||||
offset = 0;
|
||||
if (!pointToInsert.Container()->IsNodeOfType(nsINode::eTEXT)) {
|
||||
nsIContent* child = nullptr;
|
||||
if (!pointToInsert.IsStartOfContainer() &&
|
||||
(child = pointToInsert.GetPreviousSiblingOfChildAtOffset()) &&
|
||||
child->IsNodeOfType(nsINode::eTEXT)) {
|
||||
pointToInsert.Set(child, child->Length());
|
||||
} else if (!pointToInsert.IsEndOfContainer() &&
|
||||
(child = pointToInsert.GetChildAtOffset()) &&
|
||||
child->IsNodeOfType(nsINode::eTEXT)) {
|
||||
pointToInsert.Set(child, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (ShouldHandleIMEComposition()) {
|
||||
CheckedInt<int32_t> newOffset;
|
||||
if (!node->IsNodeOfType(nsINode::eTEXT)) {
|
||||
if (!pointToInsert.Container()->IsNodeOfType(nsINode::eTEXT)) {
|
||||
// create a text node
|
||||
RefPtr<nsTextNode> newNode =
|
||||
EditorBase::CreateTextNode(*aDoc, EmptyString());
|
||||
EditorBase::CreateTextNode(aDocument, EmptyString());
|
||||
// then we insert it into the dom tree
|
||||
nsresult rv = InsertNode(*newNode, *node, offset);
|
||||
nsresult rv = InsertNode(*newNode, *pointToInsert.Container(),
|
||||
pointToInsert.Offset());
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
node = newNode;
|
||||
offset = 0;
|
||||
pointToInsert.Set(newNode, 0);
|
||||
newOffset = lengthToInsert;
|
||||
} else {
|
||||
newOffset = lengthToInsert + offset;
|
||||
newOffset = lengthToInsert + pointToInsert.Offset();
|
||||
NS_ENSURE_TRUE(newOffset.isValid(), NS_ERROR_FAILURE);
|
||||
}
|
||||
nsresult rv =
|
||||
InsertTextIntoTextNodeImpl(aStringToInsert, *node->GetAsText(), offset);
|
||||
InsertTextIntoTextNodeImpl(aStringToInsert,
|
||||
*pointToInsert.Container()->GetAsText(),
|
||||
pointToInsert.Offset());
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
offset = newOffset.value();
|
||||
} else {
|
||||
if (node->IsNodeOfType(nsINode::eTEXT)) {
|
||||
CheckedInt<int32_t> newOffset = lengthToInsert + offset;
|
||||
NS_ENSURE_TRUE(newOffset.isValid(), NS_ERROR_FAILURE);
|
||||
// we are inserting text into an existing text node.
|
||||
nsresult rv =
|
||||
InsertTextIntoTextNodeImpl(aStringToInsert, *node->GetAsText(), offset);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
offset = newOffset.value();
|
||||
} else {
|
||||
// we are inserting text into a non-text node. first we have to create a
|
||||
// textnode (this also populates it with the text)
|
||||
RefPtr<nsTextNode> newNode =
|
||||
EditorBase::CreateTextNode(*aDoc, aStringToInsert);
|
||||
// then we insert it into the dom tree
|
||||
nsresult rv = InsertNode(*newNode, *node, offset);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
node = newNode;
|
||||
offset = lengthToInsert.value();
|
||||
if (aPointAfterInsertedString) {
|
||||
aPointAfterInsertedString->Set(pointToInsert.Container(),
|
||||
newOffset.value());
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
*aInOutNode = node;
|
||||
*aInOutOffset = offset;
|
||||
*aInOutChildAtOffset = nullptr;
|
||||
if (pointToInsert.Container()->IsNodeOfType(nsINode::eTEXT)) {
|
||||
CheckedInt<int32_t> newOffset = lengthToInsert + pointToInsert.Offset();
|
||||
NS_ENSURE_TRUE(newOffset.isValid(), NS_ERROR_FAILURE);
|
||||
// we are inserting text into an existing text node.
|
||||
nsresult rv =
|
||||
InsertTextIntoTextNodeImpl(aStringToInsert,
|
||||
*pointToInsert.Container()->GetAsText(),
|
||||
pointToInsert.Offset());
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
if (aPointAfterInsertedString) {
|
||||
aPointAfterInsertedString->Set(pointToInsert.Container(),
|
||||
newOffset.value());
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// we are inserting text into a non-text node. first we have to create a
|
||||
// textnode (this also populates it with the text)
|
||||
RefPtr<nsTextNode> newNode =
|
||||
EditorBase::CreateTextNode(aDocument, aStringToInsert);
|
||||
// then we insert it into the dom tree
|
||||
nsresult rv = InsertNode(*newNode, *pointToInsert.Container(),
|
||||
pointToInsert.Offset());
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
if (aPointAfterInsertedString) {
|
||||
aPointAfterInsertedString->Set(newNode, lengthToInsert.value());
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
@ -2693,7 +2680,7 @@ EditorBase::GetFirstEditableNode(nsINode* aRoot)
|
||||
|
||||
nsIContent* node = GetLeftmostChild(aRoot);
|
||||
if (node && !IsEditable(node)) {
|
||||
node = GetNextNode(node, /* aEditableNode = */ true);
|
||||
node = GetNextEditableNode(*node);
|
||||
}
|
||||
|
||||
return (node != aRoot) ? node : nullptr;
|
||||
@ -3298,104 +3285,129 @@ EditorBase::GetLengthOfDOMNode(nsIDOMNode* aNode,
|
||||
}
|
||||
|
||||
nsIContent*
|
||||
EditorBase::GetPriorNode(nsINode* aParentNode,
|
||||
int32_t aOffset,
|
||||
nsINode* aChildAtOffset,
|
||||
bool aEditableNode,
|
||||
bool aNoBlockCrossing)
|
||||
EditorBase::GetPreviousNodeInternal(nsINode& aNode,
|
||||
bool aFindEditableNode,
|
||||
bool aNoBlockCrossing)
|
||||
{
|
||||
MOZ_ASSERT(aParentNode);
|
||||
if (!IsDescendantOfEditorRoot(&aNode)) {
|
||||
return nullptr;
|
||||
}
|
||||
return FindNode(&aNode, false, aFindEditableNode, aNoBlockCrossing);
|
||||
}
|
||||
|
||||
nsIContent*
|
||||
EditorBase::GetPreviousNodeInternal(const EditorRawDOMPoint& aPoint,
|
||||
bool aFindEditableNode,
|
||||
bool aNoBlockCrossing)
|
||||
{
|
||||
MOZ_ASSERT(aPoint.IsSetAndValid());
|
||||
NS_WARNING_ASSERTION(!aPoint.Container()->IsNodeOfType(nsINode::eDATA_NODE) ||
|
||||
aPoint.Container()->IsNodeOfType(nsINode::eTEXT),
|
||||
"GetPreviousNodeInternal() doesn't assume that the start point is a "
|
||||
"data node except text node");
|
||||
|
||||
// If we are at the beginning of the node, or it is a text node, then just
|
||||
// look before it.
|
||||
if (!aOffset || aParentNode->NodeType() == nsIDOMNode::TEXT_NODE) {
|
||||
if (aNoBlockCrossing && IsBlockNode(aParentNode)) {
|
||||
if (aPoint.IsStartOfContainer() ||
|
||||
aPoint.Container()->IsNodeOfType(nsINode::eTEXT)) {
|
||||
if (aNoBlockCrossing && IsBlockNode(aPoint.Container())) {
|
||||
// If we aren't allowed to cross blocks, don't look before this block.
|
||||
return nullptr;
|
||||
}
|
||||
return GetPriorNode(aParentNode, aEditableNode, aNoBlockCrossing);
|
||||
return GetPreviousNodeInternal(*aPoint.Container(),
|
||||
aFindEditableNode, aNoBlockCrossing);
|
||||
}
|
||||
|
||||
// else look before the child at 'aOffset'
|
||||
if (aChildAtOffset) {
|
||||
return GetPriorNode(aChildAtOffset, aEditableNode, aNoBlockCrossing);
|
||||
if (aPoint.GetChildAtOffset()) {
|
||||
return GetPreviousNodeInternal(*aPoint.GetChildAtOffset(),
|
||||
aFindEditableNode, aNoBlockCrossing);
|
||||
}
|
||||
|
||||
// unless there isn't one, in which case we are at the end of the node
|
||||
// and want the deep-right child.
|
||||
nsIContent* resultNode = GetRightmostChild(aParentNode, aNoBlockCrossing);
|
||||
if (!resultNode || !aEditableNode || IsEditable(resultNode)) {
|
||||
return resultNode;
|
||||
nsIContent* rightMostNode =
|
||||
GetRightmostChild(aPoint.Container(), aNoBlockCrossing);
|
||||
if (!rightMostNode) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!aFindEditableNode || IsEditable(rightMostNode)) {
|
||||
return rightMostNode;
|
||||
}
|
||||
|
||||
// restart the search from the non-editable node we just found
|
||||
return GetPriorNode(resultNode, aEditableNode, aNoBlockCrossing);
|
||||
return GetPreviousNodeInternal(*rightMostNode,
|
||||
aFindEditableNode, aNoBlockCrossing);
|
||||
}
|
||||
|
||||
nsIContent*
|
||||
EditorBase::GetNextNode(nsINode* aParentNode,
|
||||
int32_t aOffset,
|
||||
nsINode* aChildAtOffset,
|
||||
bool aEditableNode,
|
||||
bool aNoBlockCrossing)
|
||||
EditorBase::GetNextNodeInternal(nsINode& aNode,
|
||||
bool aFindEditableNode,
|
||||
bool aNoBlockCrossing)
|
||||
{
|
||||
MOZ_ASSERT(aParentNode);
|
||||
if (!IsDescendantOfEditorRoot(&aNode)) {
|
||||
return nullptr;
|
||||
}
|
||||
return FindNode(&aNode, true, aFindEditableNode, aNoBlockCrossing);
|
||||
}
|
||||
|
||||
// if aParentNode is a text node, use its location instead
|
||||
if (aParentNode->NodeType() == nsIDOMNode::TEXT_NODE) {
|
||||
nsINode* parent = aParentNode->GetParentNode();
|
||||
NS_ENSURE_TRUE(parent, nullptr);
|
||||
aOffset = parent->IndexOf(aParentNode) + 1; // _after_ the text node
|
||||
aParentNode = parent;
|
||||
nsIContent*
|
||||
EditorBase::GetNextNodeInternal(const EditorRawDOMPoint& aPoint,
|
||||
bool aFindEditableNode,
|
||||
bool aNoBlockCrossing)
|
||||
{
|
||||
MOZ_ASSERT(aPoint.IsSetAndValid());
|
||||
NS_WARNING_ASSERTION(!aPoint.Container()->IsNodeOfType(nsINode::eDATA_NODE) ||
|
||||
aPoint.Container()->IsNodeOfType(nsINode::eTEXT),
|
||||
"GetNextNodeInternal() doesn't assume that the start point is a "
|
||||
"data node except text node");
|
||||
|
||||
EditorRawDOMPoint point(aPoint);
|
||||
|
||||
// if the container is a text node, use its location instead
|
||||
if (point.Container()->IsNodeOfType(nsINode::eTEXT)) {
|
||||
point.Set(point.Container());
|
||||
bool advanced = point.AdvanceOffset();
|
||||
if (NS_WARN_IF(!advanced)) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// look at the child at 'aOffset'
|
||||
if (aChildAtOffset) {
|
||||
if (aNoBlockCrossing && IsBlockNode(aChildAtOffset)) {
|
||||
MOZ_ASSERT(aChildAtOffset->IsContent());
|
||||
return aChildAtOffset->AsContent();
|
||||
if (point.GetChildAtOffset()) {
|
||||
if (aNoBlockCrossing && IsBlockNode(point.GetChildAtOffset())) {
|
||||
return point.GetChildAtOffset();
|
||||
}
|
||||
|
||||
nsIContent* resultNode = GetLeftmostChild(aChildAtOffset, aNoBlockCrossing);
|
||||
if (!resultNode) {
|
||||
MOZ_ASSERT(aChildAtOffset->IsContent());
|
||||
return aChildAtOffset->AsContent();
|
||||
nsIContent* leftMostNode =
|
||||
GetLeftmostChild(point.GetChildAtOffset(), aNoBlockCrossing);
|
||||
if (!leftMostNode) {
|
||||
return point.GetChildAtOffset();
|
||||
}
|
||||
|
||||
if (!IsDescendantOfEditorRoot(resultNode)) {
|
||||
if (!IsDescendantOfEditorRoot(leftMostNode)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!aEditableNode || IsEditable(resultNode)) {
|
||||
return resultNode;
|
||||
if (!aFindEditableNode || IsEditable(leftMostNode)) {
|
||||
return leftMostNode;
|
||||
}
|
||||
|
||||
// restart the search from the non-editable node we just found
|
||||
return GetNextNode(resultNode, aEditableNode, aNoBlockCrossing);
|
||||
return GetNextNodeInternal(*leftMostNode,
|
||||
aFindEditableNode, aNoBlockCrossing);
|
||||
}
|
||||
|
||||
// unless there isn't one, in which case we are at the end of the node
|
||||
// and want the next one.
|
||||
if (aNoBlockCrossing && IsBlockNode(aParentNode)) {
|
||||
if (aNoBlockCrossing && IsBlockNode(point.Container())) {
|
||||
// don't cross out of parent block
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return GetNextNode(aParentNode, aEditableNode, aNoBlockCrossing);
|
||||
}
|
||||
|
||||
nsIContent*
|
||||
EditorBase::GetPriorNode(nsINode* aCurrentNode,
|
||||
bool aEditableNode,
|
||||
bool aNoBlockCrossing /* = false */)
|
||||
{
|
||||
MOZ_ASSERT(aCurrentNode);
|
||||
|
||||
if (!IsDescendantOfEditorRoot(aCurrentNode)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return FindNode(aCurrentNode, false, aEditableNode, aNoBlockCrossing);
|
||||
return GetNextNodeInternal(*point.Container(),
|
||||
aFindEditableNode, aNoBlockCrossing);
|
||||
}
|
||||
|
||||
nsIContent*
|
||||
@ -3450,20 +3462,6 @@ EditorBase::FindNextLeafNode(nsINode* aCurrentNode,
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
nsIContent*
|
||||
EditorBase::GetNextNode(nsINode* aCurrentNode,
|
||||
bool aEditableNode,
|
||||
bool bNoBlockCrossing)
|
||||
{
|
||||
MOZ_ASSERT(aCurrentNode);
|
||||
|
||||
if (!IsDescendantOfEditorRoot(aCurrentNode)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return FindNode(aCurrentNode, true, aEditableNode, bNoBlockCrossing);
|
||||
}
|
||||
|
||||
nsIContent*
|
||||
EditorBase::FindNode(nsINode* aCurrentNode,
|
||||
bool aGoForward,
|
||||
@ -3824,20 +3822,35 @@ EditorBase::GetStartNodeAndOffset(Selection* aSelection,
|
||||
*aStartContainer = nullptr;
|
||||
*aStartOffset = 0;
|
||||
|
||||
if (!aSelection->RangeCount()) {
|
||||
EditorRawDOMPoint point = EditorBase::GetStartPoint(aSelection);
|
||||
if (!point.IsSet()) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
const nsRange* range = aSelection->GetRangeAt(0);
|
||||
NS_ENSURE_TRUE(range, NS_ERROR_FAILURE);
|
||||
|
||||
NS_ENSURE_TRUE(range->IsPositioned(), NS_ERROR_FAILURE);
|
||||
|
||||
NS_IF_ADDREF(*aStartContainer = range->GetStartContainer());
|
||||
*aStartOffset = range->StartOffset();
|
||||
NS_ADDREF(*aStartContainer = point.Container());
|
||||
*aStartOffset = point.Offset();
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// static
|
||||
EditorRawDOMPoint
|
||||
EditorBase::GetStartPoint(Selection* aSelection)
|
||||
{
|
||||
MOZ_ASSERT(aSelection);
|
||||
|
||||
if (NS_WARN_IF(!aSelection->RangeCount())) {
|
||||
return EditorRawDOMPoint();
|
||||
}
|
||||
|
||||
const nsRange* range = aSelection->GetRangeAt(0);
|
||||
if (NS_WARN_IF(!range) ||
|
||||
NS_WARN_IF(!range->IsPositioned())) {
|
||||
return EditorRawDOMPoint();
|
||||
}
|
||||
|
||||
return EditorRawDOMPoint(range->StartRef());
|
||||
}
|
||||
|
||||
/**
|
||||
* GetEndNodeAndOffset() returns whatever the end parent & offset is of
|
||||
* the first range in the selection.
|
||||
@ -3874,16 +3887,33 @@ EditorBase::GetEndNodeAndOffset(Selection* aSelection,
|
||||
*aEndContainer = nullptr;
|
||||
*aEndOffset = 0;
|
||||
|
||||
NS_ENSURE_TRUE(aSelection->RangeCount(), NS_ERROR_FAILURE);
|
||||
EditorRawDOMPoint point = EditorBase::GetEndPoint(aSelection);
|
||||
if (!point.IsSet()) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
NS_ADDREF(*aEndContainer = point.Container());
|
||||
*aEndOffset = point.Offset();
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// static
|
||||
EditorRawDOMPoint
|
||||
EditorBase::GetEndPoint(Selection* aSelection)
|
||||
{
|
||||
MOZ_ASSERT(aSelection);
|
||||
|
||||
if (NS_WARN_IF(!aSelection->RangeCount())) {
|
||||
return EditorRawDOMPoint();
|
||||
}
|
||||
|
||||
const nsRange* range = aSelection->GetRangeAt(0);
|
||||
NS_ENSURE_TRUE(range, NS_ERROR_FAILURE);
|
||||
if (NS_WARN_IF(!range) ||
|
||||
NS_WARN_IF(!range->IsPositioned())) {
|
||||
return EditorRawDOMPoint();
|
||||
}
|
||||
|
||||
NS_ENSURE_TRUE(range->IsPositioned(), NS_ERROR_FAILURE);
|
||||
|
||||
NS_IF_ADDREF(*aEndContainer = range->GetEndContainer());
|
||||
*aEndOffset = range->EndOffset();
|
||||
return NS_OK;
|
||||
return EditorRawDOMPoint(range->EndRef());
|
||||
}
|
||||
|
||||
nsresult
|
||||
@ -4580,7 +4610,7 @@ EditorBase::CreateTxnForDeleteRange(nsRange* aRangeToDelete,
|
||||
if (aAction == ePrevious && isFirst) {
|
||||
// we're backspacing from the beginning of the node. Delete the first
|
||||
// thing to our left
|
||||
nsCOMPtr<nsIContent> priorNode = GetPriorNode(node, true);
|
||||
nsCOMPtr<nsIContent> priorNode = GetPreviousEditableNode(*node);
|
||||
if (NS_WARN_IF(!priorNode)) {
|
||||
return nullptr;
|
||||
}
|
||||
@ -4619,7 +4649,7 @@ EditorBase::CreateTxnForDeleteRange(nsRange* aRangeToDelete,
|
||||
if (aAction == eNext && isLast) {
|
||||
// we're deleting from the end of the node. Delete the first thing to our
|
||||
// right
|
||||
nsCOMPtr<nsIContent> nextNode = GetNextNode(node, true);
|
||||
nsCOMPtr<nsIContent> nextNode = GetNextEditableNode(*node);
|
||||
if (NS_WARN_IF(!nextNode)) {
|
||||
return nullptr;
|
||||
}
|
||||
@ -4674,9 +4704,10 @@ EditorBase::CreateTxnForDeleteRange(nsRange* aRangeToDelete,
|
||||
// node to find out
|
||||
nsCOMPtr<nsINode> selectedNode;
|
||||
if (aAction == ePrevious) {
|
||||
selectedNode = GetPriorNode(node, offset, child, true);
|
||||
selectedNode =
|
||||
GetPreviousEditableNode(EditorRawDOMPoint(node, child, offset));
|
||||
} else if (aAction == eNext) {
|
||||
selectedNode = GetNextNode(node, offset, child, true);
|
||||
selectedNode = GetNextEditableNode(EditorRawDOMPoint(node, child, offset));
|
||||
}
|
||||
|
||||
while (selectedNode &&
|
||||
@ -4684,9 +4715,9 @@ EditorBase::CreateTxnForDeleteRange(nsRange* aRangeToDelete,
|
||||
!selectedNode->Length()) {
|
||||
// Can't delete an empty chardata node (bug 762183)
|
||||
if (aAction == ePrevious) {
|
||||
selectedNode = GetPriorNode(selectedNode, true);
|
||||
selectedNode = GetPreviousEditableNode(*selectedNode);
|
||||
} else if (aAction == eNext) {
|
||||
selectedNode = GetNextNode(selectedNode, true);
|
||||
selectedNode = GetNextEditableNode(*selectedNode);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4978,10 +5009,10 @@ EditorBase::InitializeSelection(nsIDOMEventTarget* aFocusEventTarget)
|
||||
// XXX If selection is changed during reframe, this doesn't work well!
|
||||
nsRange* firstRange = selection->GetRangeAt(0);
|
||||
NS_ENSURE_TRUE(firstRange, NS_ERROR_FAILURE);
|
||||
nsCOMPtr<nsINode> startNode = firstRange->GetStartContainer();
|
||||
int32_t startOffset = firstRange->StartOffset();
|
||||
FindBetterInsertionPoint(startNode, startOffset, nullptr);
|
||||
Text* textNode = startNode->GetAsText();
|
||||
EditorRawDOMPoint atStartOfFirstRange(firstRange->StartRef());
|
||||
EditorRawDOMPoint betterInsertionPoint =
|
||||
FindBetterInsertionPoint(atStartOfFirstRange);
|
||||
Text* textNode = betterInsertionPoint.Container()->GetAsText();
|
||||
MOZ_ASSERT(textNode,
|
||||
"There must be text node if mIMETextLength is larger than 0");
|
||||
if (textNode) {
|
||||
|
@ -286,11 +286,32 @@ public:
|
||||
public:
|
||||
virtual bool IsModifiableNode(nsINode* aNode);
|
||||
|
||||
virtual nsresult InsertTextImpl(const nsAString& aStringToInsert,
|
||||
nsCOMPtr<nsINode>* aInOutNode,
|
||||
nsCOMPtr<nsIContent>* aInOutChildAtOffset,
|
||||
int32_t* aInOutOffset,
|
||||
nsIDocument* aDoc);
|
||||
/**
|
||||
* InsertTextImpl() inserts aStringToInsert to aPointToInsert or better
|
||||
* insertion point around it. If aPointToInsert isn't in a text node,
|
||||
* this method looks for the nearest point in a text node with
|
||||
* FindBetterInsertionPoint(). If there is no text node, this creates
|
||||
* new text node and put aStringToInsert to it.
|
||||
*
|
||||
* @param aDocument The document of this editor.
|
||||
* @param aStringToInsert The string to insert.
|
||||
* @param aPointToInser The point to insert aStringToInsert.
|
||||
* Must be valid DOM point.
|
||||
* @param aPointAfterInsertedString
|
||||
* The point after inserted aStringToInsert.
|
||||
* So, when this method actually inserts string,
|
||||
* this is set to a point in the text node.
|
||||
* Otherwise, this may be set to aPointToInsert.
|
||||
* @return When this succeeds to insert the string or
|
||||
* does nothing during composition, returns NS_OK.
|
||||
* Otherwise, an error code.
|
||||
*/
|
||||
virtual nsresult
|
||||
InsertTextImpl(nsIDocument& aDocument,
|
||||
const nsAString& aStringToInsert,
|
||||
const EditorRawDOMPoint& aPointToInsert,
|
||||
EditorRawDOMPoint* aPointAfterInsertedString = nullptr);
|
||||
|
||||
nsresult InsertTextIntoTextNodeImpl(const nsAString& aStringToInsert,
|
||||
Text& aTextNode, int32_t aOffset,
|
||||
bool aSuppressIME = false);
|
||||
@ -586,11 +607,60 @@ protected:
|
||||
virtual bool IsBlockNode(nsINode* aNode);
|
||||
|
||||
/**
|
||||
* Helper for GetPriorNode() and GetNextNode().
|
||||
* Helper for GetPreviousNodeInternal() and GetNextNodeInternal().
|
||||
*/
|
||||
nsIContent* FindNextLeafNode(nsINode* aCurrentNode,
|
||||
bool aGoForward,
|
||||
bool bNoBlockCrossing);
|
||||
nsIContent* FindNode(nsINode* aCurrentNode,
|
||||
bool aGoForward,
|
||||
bool aEditableNode,
|
||||
bool bNoBlockCrossing);
|
||||
|
||||
/**
|
||||
* Get the node immediately previous node of aNode.
|
||||
* @param atNode The node from which we start the search.
|
||||
* @param aFindEditableNode If true, only return an editable node.
|
||||
* @param aNoBlockCrossing If true, don't move across "block" nodes,
|
||||
* whatever that means.
|
||||
* @return The node that occurs before aNode in
|
||||
* the tree, skipping non-editable nodes if
|
||||
* aFindEditableNode is true. If there is no
|
||||
* previous node, returns nullptr.
|
||||
*/
|
||||
nsIContent* GetPreviousNodeInternal(nsINode& aNode,
|
||||
bool aFindEditableNode,
|
||||
bool aNoBlockCrossing);
|
||||
|
||||
/**
|
||||
* And another version that takes a point in DOM tree rather than a node.
|
||||
*/
|
||||
nsIContent* GetPreviousNodeInternal(const EditorRawDOMPoint& aPoint,
|
||||
bool aFindEditableNode,
|
||||
bool aNoBlockCrossing);
|
||||
|
||||
/**
|
||||
* Get the node immediately next node of aNode.
|
||||
* @param aNode The node from which we start the search.
|
||||
* @param aFindEditableNode If true, only return an editable node.
|
||||
* @param aNoBlockCrossing If true, don't move across "block" nodes,
|
||||
* whatever that means.
|
||||
* @return The node that occurs after aNode in the
|
||||
* tree, skipping non-editable nodes if
|
||||
* aFindEditableNode is true. If there is no
|
||||
* next node, returns nullptr.
|
||||
*/
|
||||
nsIContent* GetNextNodeInternal(nsINode& aNode,
|
||||
bool aFindEditableNode,
|
||||
bool bNoBlockCrossing);
|
||||
|
||||
/**
|
||||
* And another version that takes a point in DOM tree rather than a node.
|
||||
*/
|
||||
nsIContent* GetNextNodeInternal(const EditorRawDOMPoint& aPoint,
|
||||
bool aFindEditableNode,
|
||||
bool aNoBlockCrossing);
|
||||
|
||||
|
||||
virtual nsresult InstallEventListeners();
|
||||
virtual void CreateEventListeners();
|
||||
@ -716,58 +786,102 @@ public:
|
||||
static nsresult GetLengthOfDOMNode(nsIDOMNode *aNode, uint32_t &aCount);
|
||||
|
||||
/**
|
||||
* Get the node immediately prior to aCurrentNode.
|
||||
* @param aCurrentNode the node from which we start the search
|
||||
* @param aEditableNode if true, only return an editable node
|
||||
* @param aResultNode [OUT] the node that occurs before aCurrentNode in
|
||||
* the tree, skipping non-editable nodes if
|
||||
* aEditableNode is true. If there is no prior
|
||||
* node, aResultNode will be nullptr.
|
||||
* @param bNoBlockCrossing If true, don't move across "block" nodes,
|
||||
* whatever that means.
|
||||
* Get the previous node.
|
||||
*/
|
||||
nsIContent* GetPriorNode(nsINode* aCurrentNode, bool aEditableNode,
|
||||
bool aNoBlockCrossing = false);
|
||||
nsIContent* GetPreviousNode(const EditorRawDOMPoint& aPoint)
|
||||
{
|
||||
return GetPreviousNodeInternal(aPoint, false, false);
|
||||
}
|
||||
nsIContent* GetPreviousEditableNode(const EditorRawDOMPoint& aPoint)
|
||||
{
|
||||
return GetPreviousNodeInternal(aPoint, true, false);
|
||||
}
|
||||
nsIContent* GetPreviousNodeInBlock(const EditorRawDOMPoint& aPoint)
|
||||
{
|
||||
return GetPreviousNodeInternal(aPoint, false, true);
|
||||
}
|
||||
nsIContent* GetPreviousEditableNodeInBlock(
|
||||
const EditorRawDOMPoint& aPoint)
|
||||
{
|
||||
return GetPreviousNodeInternal(aPoint, true, true);
|
||||
}
|
||||
nsIContent* GetPreviousNode(nsINode& aNode)
|
||||
{
|
||||
return GetPreviousNodeInternal(aNode, false, false);
|
||||
}
|
||||
nsIContent* GetPreviousEditableNode(nsINode& aNode)
|
||||
{
|
||||
return GetPreviousNodeInternal(aNode, true, false);
|
||||
}
|
||||
nsIContent* GetPreviousNodeInBlock(nsINode& aNode)
|
||||
{
|
||||
return GetPreviousNodeInternal(aNode, false, true);
|
||||
}
|
||||
nsIContent* GetPreviousEditableNodeInBlock(nsINode& aNode)
|
||||
{
|
||||
return GetPreviousNodeInternal(aNode, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* And another version that takes a {parent,offset} pair rather than a node.
|
||||
* Get the next node.
|
||||
*
|
||||
* Note that methods taking EditorRawDOMPoint behavior includes the
|
||||
* child at offset as search target. E.g., following code causes infinite
|
||||
* loop.
|
||||
*
|
||||
* EditorRawDOMPoint point(aEditableNode);
|
||||
* while (nsIContent* content = GetNextEditableNode(point)) {
|
||||
* // Do something...
|
||||
* point.Set(content);
|
||||
* }
|
||||
*
|
||||
* Following code must be you expected:
|
||||
*
|
||||
* while (nsIContent* content = GetNextEditableNode(point)) {
|
||||
* // Do something...
|
||||
* DebugOnly<bool> advanced = point.Advanced();
|
||||
* MOZ_ASSERT(advanced);
|
||||
* point.Set(point.GetChildAtOffset());
|
||||
* }
|
||||
*
|
||||
* On the other hand, the methods taking nsINode behavior must be what
|
||||
* you want. They start to search the result from next node of the given
|
||||
* node.
|
||||
*/
|
||||
nsIContent* GetPriorNode(nsINode* aParentNode,
|
||||
int32_t aOffset,
|
||||
nsINode* aChildAtOffset,
|
||||
bool aEditableNode,
|
||||
bool aNoBlockCrossing = false);
|
||||
nsIContent* GetNextNode(const EditorRawDOMPoint& aPoint)
|
||||
{
|
||||
return GetNextNodeInternal(aPoint, false, false);
|
||||
}
|
||||
nsIContent* GetNextEditableNode(const EditorRawDOMPoint& aPoint)
|
||||
{
|
||||
return GetNextNodeInternal(aPoint, true, false);
|
||||
}
|
||||
nsIContent* GetNextNodeInBlock(const EditorRawDOMPoint& aPoint)
|
||||
{
|
||||
return GetNextNodeInternal(aPoint, false, true);
|
||||
}
|
||||
nsIContent* GetNextEditableNodeInBlock(
|
||||
const EditorRawDOMPoint& aPoint)
|
||||
{
|
||||
return GetNextNodeInternal(aPoint, true, true);
|
||||
}
|
||||
nsIContent* GetNextNode(nsINode& aNode)
|
||||
{
|
||||
return GetNextNodeInternal(aNode, false, false);
|
||||
}
|
||||
nsIContent* GetNextEditableNode(nsINode& aNode)
|
||||
{
|
||||
return GetNextNodeInternal(aNode, true, false);
|
||||
}
|
||||
nsIContent* GetNextNodeInBlock(nsINode& aNode)
|
||||
{
|
||||
return GetNextNodeInternal(aNode, false, true);
|
||||
}
|
||||
nsIContent* GetNextEditableNodeInBlock(nsINode& aNode)
|
||||
{
|
||||
return GetNextNodeInternal(aNode, true, true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the node immediately after to aCurrentNode.
|
||||
* @param aCurrentNode the node from which we start the search
|
||||
* @param aEditableNode if true, only return an editable node
|
||||
* @param aResultNode [OUT] the node that occurs after aCurrentNode in the
|
||||
* tree, skipping non-editable nodes if
|
||||
* aEditableNode is true. If there is no prior
|
||||
* node, aResultNode will be nullptr.
|
||||
*/
|
||||
nsIContent* GetNextNode(nsINode* aCurrentNode,
|
||||
bool aEditableNode,
|
||||
bool bNoBlockCrossing = false);
|
||||
|
||||
/**
|
||||
* And another version that takes a {parent,offset} pair rather than a node.
|
||||
*/
|
||||
nsIContent* GetNextNode(nsINode* aParentNode,
|
||||
int32_t aOffset,
|
||||
nsINode* aChildAtOffset,
|
||||
bool aEditableNode,
|
||||
bool aNoBlockCrossing = false);
|
||||
|
||||
/**
|
||||
* Helper for GetNextNode() and GetPriorNode().
|
||||
*/
|
||||
nsIContent* FindNode(nsINode* aCurrentNode,
|
||||
bool aGoForward,
|
||||
bool aEditableNode,
|
||||
bool bNoBlockCrossing);
|
||||
/**
|
||||
* Get the rightmost child of aCurrentNode;
|
||||
* return nullptr if aCurrentNode has no children.
|
||||
@ -927,12 +1041,14 @@ public:
|
||||
static nsresult GetStartNodeAndOffset(Selection* aSelection,
|
||||
nsINode** aStartContainer,
|
||||
int32_t* aStartOffset);
|
||||
static EditorRawDOMPoint GetStartPoint(Selection* aSelection);
|
||||
static nsresult GetEndNodeAndOffset(Selection* aSelection,
|
||||
nsIDOMNode** outEndNode,
|
||||
int32_t* outEndOffset);
|
||||
static nsresult GetEndNodeAndOffset(Selection* aSelection,
|
||||
nsINode** aEndContainer,
|
||||
int32_t* aEndOffset);
|
||||
static EditorRawDOMPoint GetEndPoint(Selection* aSelection);
|
||||
|
||||
static nsresult GetEndChildNode(Selection* aSelection,
|
||||
nsIContent** aEndNode);
|
||||
@ -1258,22 +1374,11 @@ public:
|
||||
* FindBetterInsertionPoint() tries to look for better insertion point which
|
||||
* is typically the nearest text node and offset in it.
|
||||
*
|
||||
* @param aNode in/out param, on input set to the node to use to start the search,
|
||||
* on output set to the node found as the better insertion point.
|
||||
* @param aOffset in/out param, on input set to the offset to use to start the
|
||||
* search, on putput set to the offset found as the better insertion
|
||||
* point.
|
||||
* @param aSelChild in/out param, on input, can be set to nullptr if the caller
|
||||
* doesn't want to pass this in, or set to a pointer to an nsCOMPtr
|
||||
* pointing to the child at the input node and offset, and on output
|
||||
* the method will make it point to the child at the output node and
|
||||
* offset returned in aNode and aOffset.
|
||||
* @param aPoint Insertion point which the callers found.
|
||||
* @return Better insertion point if there is. If not returns
|
||||
* same point as aPoint.
|
||||
*/
|
||||
void FindBetterInsertionPoint(nsCOMPtr<nsIDOMNode>& aNode,
|
||||
int32_t& aOffset);
|
||||
void FindBetterInsertionPoint(nsCOMPtr<nsINode>& aNode,
|
||||
int32_t& aOffset,
|
||||
nsCOMPtr<nsIContent>* aSelChild);
|
||||
EditorRawDOMPoint FindBetterInsertionPoint(const EditorRawDOMPoint& aPoint);
|
||||
|
||||
/**
|
||||
* HideCaret() hides caret with nsCaret::AddForceHide() or may show carent
|
||||
|
@ -84,6 +84,19 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
EditorDOMPointBase<nsINode*, nsIContent*>
|
||||
AsRaw() const
|
||||
{
|
||||
return EditorDOMPointBase<nsINode*, nsIContent*>(*this);
|
||||
}
|
||||
|
||||
template<typename A, typename B>
|
||||
EditorDOMPointBase& operator=(const EditorDOMPointBase<A, B>& aOther)
|
||||
{
|
||||
RangeBoundaryBase<ParentType, RefType>::operator=(aOther);
|
||||
return *this;
|
||||
}
|
||||
|
||||
private:
|
||||
static nsIContent* GetRef(nsINode* aContainerNode, nsIContent* aPointedNode)
|
||||
{
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -410,11 +410,22 @@ protected:
|
||||
void CheckInterlinePosition(Selection& aSelection);
|
||||
nsresult AdjustSelection(Selection* aSelection,
|
||||
nsIEditor::EDirection aAction);
|
||||
nsresult FindNearSelectableNode(nsINode* aSelNode,
|
||||
int32_t aSelOffset,
|
||||
nsINode* aChildAtOffset,
|
||||
nsIEditor::EDirection& aDirection,
|
||||
nsCOMPtr<nsIContent>* outSelectableNode);
|
||||
|
||||
/**
|
||||
* FindNearEditableNode() tries to find an editable node near aPoint.
|
||||
*
|
||||
* @param aPoint The DOM point where to start to search from.
|
||||
* @param aDirection If nsIEditor::ePrevious is set, this searches an
|
||||
* editable node from next nodes. Otherwise, from
|
||||
* previous nodes.
|
||||
* @return If found, returns non-nullptr. Otherwise, nullptr.
|
||||
* Note that if found node is in different table element,
|
||||
* this returns nullptr.
|
||||
* And also if aDirection is not nsIEditor::ePrevious,
|
||||
* the result may be the node pointed by aPoint.
|
||||
*/
|
||||
nsIContent* FindNearEditableNode(const EditorRawDOMPoint& aPoint,
|
||||
nsIEditor::EDirection aDirection);
|
||||
/**
|
||||
* Returns true if aNode1 or aNode2 or both is the descendant of some type of
|
||||
* table element, but their nearest table element ancestors differ. "Table
|
||||
|
@ -876,7 +876,7 @@ HTMLEditor::IsVisibleBRElement(nsINode* aNode)
|
||||
return false;
|
||||
}
|
||||
// Check if there is a later node in block after br
|
||||
nsCOMPtr<nsINode> nextNode = GetNextHTMLNode(aNode, true);
|
||||
nsCOMPtr<nsINode> nextNode = GetNextEditableHTMLNodeInBlock(*aNode);
|
||||
if (nextNode && TextEditUtils::IsBreak(nextNode)) {
|
||||
return true;
|
||||
}
|
||||
@ -895,7 +895,7 @@ HTMLEditor::IsVisibleBRElement(nsINode* aNode)
|
||||
|
||||
// If there's an inline node after this one that's not a break, and also a
|
||||
// prior break, this break must be visible.
|
||||
nsCOMPtr<nsINode> priorNode = GetPriorHTMLNode(aNode, true);
|
||||
nsCOMPtr<nsINode> priorNode = GetPreviousEditableHTMLNodeInBlock(*aNode);
|
||||
if (priorNode && TextEditUtils::IsBreak(priorNode)) {
|
||||
return true;
|
||||
}
|
||||
@ -3186,20 +3186,22 @@ HTMLEditor::DeleteText(nsGenericDOMDataNode& aCharData,
|
||||
}
|
||||
|
||||
nsresult
|
||||
HTMLEditor::InsertTextImpl(const nsAString& aStringToInsert,
|
||||
nsCOMPtr<nsINode>* aInOutNode,
|
||||
nsCOMPtr<nsIContent>* aInOutChildAtOffset,
|
||||
int32_t* aInOutOffset,
|
||||
nsIDocument* aDoc)
|
||||
HTMLEditor::InsertTextImpl(nsIDocument& aDocument,
|
||||
const nsAString& aStringToInsert,
|
||||
const EditorRawDOMPoint& aPointToInsert,
|
||||
EditorRawDOMPoint* aPointAfterInsertedString)
|
||||
{
|
||||
if (NS_WARN_IF(!aPointToInsert.IsSet())) {
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
// Do nothing if the node is read-only
|
||||
if (!IsModifiableNode(*aInOutNode)) {
|
||||
if (!IsModifiableNode(aPointToInsert.Container())) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
return EditorBase::InsertTextImpl(aStringToInsert, aInOutNode,
|
||||
aInOutChildAtOffset,
|
||||
aInOutOffset, aDoc);
|
||||
return EditorBase::InsertTextImpl(aDocument, aStringToInsert, aPointToInsert,
|
||||
aPointAfterInsertedString);
|
||||
}
|
||||
|
||||
void
|
||||
@ -3865,76 +3867,48 @@ HTMLEditor::GetNextHTMLSibling(nsINode* aNode)
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetPriorHTMLNode() returns the previous editable leaf node, if there is
|
||||
* one within the <body>.
|
||||
*/
|
||||
nsIContent*
|
||||
HTMLEditor::GetPriorHTMLNode(nsINode* aNode,
|
||||
bool aNoBlockCrossing)
|
||||
HTMLEditor::GetPreviousEditableHTMLNodeInternal(nsINode& aNode,
|
||||
bool aNoBlockCrossing)
|
||||
{
|
||||
MOZ_ASSERT(aNode);
|
||||
|
||||
if (!GetActiveEditingHost()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return GetPriorNode(aNode, true, aNoBlockCrossing);
|
||||
return aNoBlockCrossing ? GetPreviousEditableNodeInBlock(aNode) :
|
||||
GetPreviousEditableNode(aNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetPriorHTMLNode() is same as above but takes {parent,offset} instead of
|
||||
* node.
|
||||
*/
|
||||
nsIContent*
|
||||
HTMLEditor::GetPriorHTMLNode(nsINode* aParent,
|
||||
int32_t aOffset,
|
||||
nsINode* aChildAtOffset,
|
||||
bool aNoBlockCrossing)
|
||||
HTMLEditor::GetPreviousEditableHTMLNodeInternal(const EditorRawDOMPoint& aPoint,
|
||||
bool aNoBlockCrossing)
|
||||
{
|
||||
MOZ_ASSERT(aParent);
|
||||
|
||||
if (!GetActiveEditingHost()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return GetPriorNode(aParent, aOffset, aChildAtOffset, true, aNoBlockCrossing);
|
||||
return aNoBlockCrossing ? GetPreviousEditableNodeInBlock(aPoint) :
|
||||
GetPreviousEditableNode(aPoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetNextHTMLNode() returns the next editable leaf node, if there is
|
||||
* one within the <body>.
|
||||
*/
|
||||
nsIContent*
|
||||
HTMLEditor::GetNextHTMLNode(nsINode* aNode,
|
||||
bool aNoBlockCrossing)
|
||||
HTMLEditor::GetNextEditableHTMLNodeInternal(nsINode& aNode,
|
||||
bool aNoBlockCrossing)
|
||||
{
|
||||
MOZ_ASSERT(aNode);
|
||||
|
||||
nsIContent* result = GetNextNode(aNode, true, aNoBlockCrossing);
|
||||
|
||||
if (result && !IsDescendantOfEditorRoot(result)) {
|
||||
if (!GetActiveEditingHost()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return result;
|
||||
return aNoBlockCrossing ? GetNextEditableNodeInBlock(aNode) :
|
||||
GetNextEditableNode(aNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetNextHTMLNode() is same as above but takes {parent,offset} instead of node.
|
||||
*/
|
||||
nsIContent*
|
||||
HTMLEditor::GetNextHTMLNode(nsINode* aParent,
|
||||
int32_t aOffset,
|
||||
nsINode* aChildAtOffset,
|
||||
bool aNoBlockCrossing)
|
||||
HTMLEditor::GetNextEditableHTMLNodeInternal(const EditorRawDOMPoint& aPoint,
|
||||
bool aNoBlockCrossing)
|
||||
{
|
||||
nsIContent* content = GetNextNode(aParent, aOffset, aChildAtOffset,
|
||||
true, aNoBlockCrossing);
|
||||
if (content && !IsDescendantOfEditorRoot(content)) {
|
||||
if (!GetActiveEditingHost()) {
|
||||
return nullptr;
|
||||
}
|
||||
return content;
|
||||
return aNoBlockCrossing ? GetNextEditableNodeInBlock(aPoint) :
|
||||
GetNextEditableNode(aPoint);
|
||||
}
|
||||
|
||||
bool
|
||||
@ -3990,7 +3964,7 @@ HTMLEditor::GetFirstEditableLeaf(nsINode& aNode)
|
||||
{
|
||||
nsCOMPtr<nsIContent> child = GetLeftmostChild(&aNode);
|
||||
while (child && (!IsEditable(child) || child->HasChildren())) {
|
||||
child = GetNextHTMLNode(child);
|
||||
child = GetNextEditableHTMLNode(*child);
|
||||
|
||||
// Only accept nodes that are descendants of aNode
|
||||
if (!aNode.Contains(child)) {
|
||||
@ -4006,7 +3980,7 @@ HTMLEditor::GetLastEditableLeaf(nsINode& aNode)
|
||||
{
|
||||
nsCOMPtr<nsIContent> child = GetRightmostChild(&aNode, false);
|
||||
while (child && (!IsEditable(child) || child->HasChildren())) {
|
||||
child = GetPriorHTMLNode(child);
|
||||
child = GetPreviousEditableHTMLNode(*child);
|
||||
|
||||
// Only accept nodes that are descendants of aNode
|
||||
if (!aNode.Contains(child)) {
|
||||
@ -4521,7 +4495,7 @@ HTMLEditor::CopyLastEditableChildStyles(nsINode* aPreviousBlock,
|
||||
tmp = GetLastEditableChild(*child);
|
||||
}
|
||||
while (child && TextEditUtils::IsBreak(child)) {
|
||||
child = GetPriorHTMLNode(child);
|
||||
child = GetPreviousEditableHTMLNode(*child);
|
||||
}
|
||||
nsCOMPtr<Element> newStyles, deepestStyle;
|
||||
nsCOMPtr<nsINode> childNode = child;
|
||||
|
@ -321,11 +321,12 @@ public:
|
||||
NS_IMETHOD DeleteNode(nsIDOMNode* aNode) override;
|
||||
nsresult DeleteText(nsGenericDOMDataNode& aTextNode, uint32_t aOffset,
|
||||
uint32_t aLength);
|
||||
virtual nsresult InsertTextImpl(const nsAString& aStringToInsert,
|
||||
nsCOMPtr<nsINode>* aInOutNode,
|
||||
nsCOMPtr<nsIContent>* aInOutChildAtOffset,
|
||||
int32_t* aInOutOffset,
|
||||
nsIDocument* aDoc) override;
|
||||
virtual nsresult
|
||||
InsertTextImpl(nsIDocument& aDocument,
|
||||
const nsAString& aStringToInsert,
|
||||
const EditorRawDOMPoint& aPointToInsert,
|
||||
EditorRawDOMPoint* aPointAfterInsertedString =
|
||||
nullptr) override;
|
||||
NS_IMETHOD_(bool) IsModifiableNode(nsIDOMNode* aNode) override;
|
||||
virtual bool IsModifiableNode(nsINode* aNode) override;
|
||||
|
||||
@ -776,15 +777,76 @@ protected:
|
||||
|
||||
nsIContent* GetNextHTMLSibling(nsINode* aNode);
|
||||
|
||||
nsIContent* GetPriorHTMLNode(nsINode* aNode, bool aNoBlockCrossing = false);
|
||||
nsIContent* GetPriorHTMLNode(nsINode* aParent, int32_t aOffset,
|
||||
nsINode* aChildAtOffset,
|
||||
bool aNoBlockCrossing = false);
|
||||
/**
|
||||
* GetPreviousEditableHTMLNode*() methods are similar to
|
||||
* EditorBase::GetPreviousEditableNode() but this won't return nodes outside
|
||||
* active editing host.
|
||||
*/
|
||||
nsIContent* GetPreviousEditableHTMLNode(nsINode& aNode)
|
||||
{
|
||||
return GetPreviousEditableHTMLNodeInternal(aNode, false);
|
||||
}
|
||||
nsIContent* GetPreviousEditableHTMLNodeInBlock(nsINode& aNode)
|
||||
{
|
||||
return GetPreviousEditableHTMLNodeInternal(aNode, true);
|
||||
}
|
||||
nsIContent* GetPreviousEditableHTMLNode(const EditorRawDOMPoint& aPoint)
|
||||
{
|
||||
return GetPreviousEditableHTMLNodeInternal(aPoint, false);
|
||||
}
|
||||
nsIContent* GetPreviousEditableHTMLNodeInBlock(
|
||||
const EditorRawDOMPoint& aPoint)
|
||||
{
|
||||
return GetPreviousEditableHTMLNodeInternal(aPoint, true);
|
||||
}
|
||||
|
||||
nsIContent* GetNextHTMLNode(nsINode* aNode, bool aNoBlockCrossing = false);
|
||||
nsIContent* GetNextHTMLNode(nsINode* aParent, int32_t aOffset,
|
||||
nsINode* aChildAtOffset,
|
||||
bool aNoBlockCrossing = false);
|
||||
/**
|
||||
* GetPreviousEditableHTMLNodeInternal() methods are common implementation
|
||||
* of above methods. Please don't use this method directly.
|
||||
*/
|
||||
nsIContent* GetPreviousEditableHTMLNodeInternal(nsINode& aNode,
|
||||
bool aNoBlockCrossing);
|
||||
nsIContent* GetPreviousEditableHTMLNodeInternal(
|
||||
const EditorRawDOMPoint& aPoint,
|
||||
bool aNoBlockCrossing);
|
||||
|
||||
/**
|
||||
* GetNextEditableHTMLNode*() methods are similar to
|
||||
* EditorBase::GetNextEditableNode() but this won't return nodes outside
|
||||
* active editing host.
|
||||
*
|
||||
* Note that same as EditorBaseGetTextEditableNode(), methods which take
|
||||
* |const EditorRawDOMPoint&| start to search from the node pointed by it.
|
||||
* On the other hand, methods which take |nsINode&| start to search from
|
||||
* next node of aNode.
|
||||
*/
|
||||
nsIContent* GetNextEditableHTMLNode(nsINode& aNode)
|
||||
{
|
||||
return GetNextEditableHTMLNodeInternal(aNode, false);
|
||||
}
|
||||
nsIContent* GetNextEditableHTMLNodeInBlock(nsINode& aNode)
|
||||
{
|
||||
return GetNextEditableHTMLNodeInternal(aNode, true);
|
||||
}
|
||||
nsIContent* GetNextEditableHTMLNode(const EditorRawDOMPoint& aPoint)
|
||||
{
|
||||
return GetNextEditableHTMLNodeInternal(aPoint, false);
|
||||
}
|
||||
nsIContent* GetNextEditableHTMLNodeInBlock(
|
||||
const EditorRawDOMPoint& aPoint)
|
||||
{
|
||||
return GetNextEditableHTMLNodeInternal(aPoint, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetNextEditableHTMLNodeInternal() methods are common implementation
|
||||
* of above methods. Please don't use this method directly.
|
||||
*/
|
||||
nsIContent* GetNextEditableHTMLNodeInternal(nsINode& aNode,
|
||||
bool aNoBlockCrossing);
|
||||
nsIContent* GetNextEditableHTMLNodeInternal(
|
||||
const EditorRawDOMPoint& aPoint,
|
||||
bool aNoBlockCrossing);
|
||||
|
||||
bool IsFirstEditableChild(nsINode* aNode);
|
||||
bool IsLastEditableChild(nsINode* aNode);
|
||||
|
@ -729,16 +729,16 @@ TextEditRules::WillInsertText(EditAction aAction,
|
||||
|
||||
// get the (collapsed) selection location
|
||||
NS_ENSURE_STATE(aSelection->GetRangeAt(0));
|
||||
nsCOMPtr<nsINode> selNode = aSelection->GetRangeAt(0)->GetStartContainer();
|
||||
nsCOMPtr<nsIContent> selChild =
|
||||
aSelection->GetRangeAt(0)->GetChildAtStartOffset();
|
||||
int32_t selOffset = aSelection->GetRangeAt(0)->StartOffset();
|
||||
NS_ENSURE_STATE(selNode);
|
||||
EditorRawDOMPoint atStartOfSelection(aSelection->GetRangeAt(0)->StartRef());
|
||||
if (NS_WARN_IF(!atStartOfSelection.IsSetAndValid())) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
// don't put text in places that can't have it
|
||||
NS_ENSURE_STATE(mTextEditor);
|
||||
if (!EditorBase::IsTextNode(selNode) &&
|
||||
!mTextEditor->CanContainTag(*selNode, *nsGkAtoms::textTagName)) {
|
||||
if (!EditorBase::IsTextNode(atStartOfSelection.Container()) &&
|
||||
!mTextEditor->CanContainTag(*atStartOfSelection.Container(),
|
||||
*nsGkAtoms::textTagName)) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
@ -750,41 +750,46 @@ TextEditRules::WillInsertText(EditAction aAction,
|
||||
if (aAction == EditAction::insertIMEText) {
|
||||
NS_ENSURE_STATE(mTextEditor);
|
||||
// Find better insertion point to insert text.
|
||||
mTextEditor->FindBetterInsertionPoint(selNode, selOffset,
|
||||
address_of(selChild));
|
||||
EditorRawDOMPoint betterInsertionPoint =
|
||||
mTextEditor->FindBetterInsertionPoint(atStartOfSelection);
|
||||
// If there is one or more IME selections, its minimum offset should be
|
||||
// the insertion point.
|
||||
int32_t IMESelectionOffset =
|
||||
mTextEditor->GetIMESelectionStartOffsetIn(selNode);
|
||||
mTextEditor->GetIMESelectionStartOffsetIn(
|
||||
betterInsertionPoint.Container());
|
||||
if (IMESelectionOffset >= 0) {
|
||||
selOffset = IMESelectionOffset;
|
||||
betterInsertionPoint.Set(betterInsertionPoint.Container(),
|
||||
IMESelectionOffset);
|
||||
}
|
||||
rv = mTextEditor->InsertTextImpl(*outString, address_of(selNode),
|
||||
address_of(selChild), &selOffset, doc);
|
||||
rv = mTextEditor->InsertTextImpl(*doc, *outString, betterInsertionPoint);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
} else {
|
||||
// aAction == EditAction::insertText; find where we are
|
||||
nsCOMPtr<nsINode> curNode = selNode;
|
||||
int32_t curOffset = selOffset;
|
||||
// aAction == EditAction::insertText
|
||||
|
||||
// don't change my selection in subtransactions
|
||||
NS_ENSURE_STATE(mTextEditor);
|
||||
AutoTransactionsConserveSelection dontChangeMySelection(mTextEditor);
|
||||
|
||||
rv = mTextEditor->InsertTextImpl(*outString, address_of(curNode),
|
||||
address_of(selChild), &curOffset, doc);
|
||||
EditorRawDOMPoint pointAfterStringInserted;
|
||||
rv = mTextEditor->InsertTextImpl(*doc, *outString, atStartOfSelection,
|
||||
&pointAfterStringInserted);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
if (curNode) {
|
||||
if (pointAfterStringInserted.IsSet()) {
|
||||
// Make the caret attach to the inserted text, unless this text ends with a LF,
|
||||
// in which case make the caret attach to the next line.
|
||||
bool endsWithLF =
|
||||
!outString->IsEmpty() && outString->Last() == nsCRT::LF;
|
||||
aSelection->SetInterlinePosition(endsWithLF);
|
||||
|
||||
MOZ_ASSERT(!selChild,
|
||||
"After inserting text into a text node, selChild should be nullptr");
|
||||
aSelection->Collapse(curNode, curOffset);
|
||||
MOZ_ASSERT(!pointAfterStringInserted.GetChildAtOffset(),
|
||||
"After inserting text into a text node, pointAfterStringInserted."
|
||||
"GetChildAtOffset() should be nullptr");
|
||||
ErrorResult error;
|
||||
aSelection->Collapse(pointAfterStringInserted, error);
|
||||
if (error.Failed()) {
|
||||
NS_WARNING("Failed to collapse selection after inserting string");
|
||||
}
|
||||
}
|
||||
}
|
||||
ASSERT_PASSWORD_LENGTHS_EQUAL()
|
||||
|
@ -532,63 +532,89 @@ TextEditor::ExtendSelectionForDelete(Selection* aSelection,
|
||||
GetSelectionController(getter_AddRefs(selCont));
|
||||
NS_ENSURE_TRUE(selCont, NS_ERROR_NO_INTERFACE);
|
||||
|
||||
nsresult rv;
|
||||
switch (*aAction) {
|
||||
case eNextWord:
|
||||
rv = selCont->WordExtendForDelete(true);
|
||||
case eNextWord: {
|
||||
nsresult rv = selCont->WordExtendForDelete(true);
|
||||
// DeleteSelectionImpl doesn't handle these actions
|
||||
// because it's inside batching, so don't confuse it:
|
||||
*aAction = eNone;
|
||||
break;
|
||||
case ePreviousWord:
|
||||
rv = selCont->WordExtendForDelete(false);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
case ePreviousWord: {
|
||||
nsresult rv = selCont->WordExtendForDelete(false);
|
||||
*aAction = eNone;
|
||||
break;
|
||||
case eNext:
|
||||
rv = selCont->CharacterExtendForDelete();
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
case eNext: {
|
||||
nsresult rv = selCont->CharacterExtendForDelete();
|
||||
// Don't set aAction to eNone (see Bug 502259)
|
||||
break;
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
case ePrevious: {
|
||||
// Only extend the selection where the selection is after a UTF-16
|
||||
// surrogate pair or a variation selector.
|
||||
// For other cases we don't want to do that, in order
|
||||
// to make sure that pressing backspace will only delete the last
|
||||
// typed character.
|
||||
nsCOMPtr<nsINode> node;
|
||||
int32_t offset;
|
||||
rv = GetStartNodeAndOffset(aSelection, getter_AddRefs(node), &offset);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
NS_ENSURE_TRUE(node, NS_ERROR_FAILURE);
|
||||
EditorRawDOMPoint atStartOfSelection =
|
||||
EditorBase::GetStartPoint(aSelection);
|
||||
if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
// node might be anonymous DIV, so we find better text node
|
||||
FindBetterInsertionPoint(node, offset, nullptr);
|
||||
EditorRawDOMPoint insertionPoint =
|
||||
FindBetterInsertionPoint(atStartOfSelection);
|
||||
|
||||
if (IsTextNode(node)) {
|
||||
const nsTextFragment* data = node->GetAsText()->GetText();
|
||||
if (IsTextNode(insertionPoint.Container())) {
|
||||
const nsTextFragment* data =
|
||||
insertionPoint.Container()->GetAsText()->GetText();
|
||||
uint32_t offset = insertionPoint.Offset();
|
||||
if ((offset > 1 &&
|
||||
NS_IS_LOW_SURROGATE(data->CharAt(offset - 1)) &&
|
||||
NS_IS_HIGH_SURROGATE(data->CharAt(offset - 2))) ||
|
||||
(offset > 0 &&
|
||||
gfxFontUtils::IsVarSelector(data->CharAt(offset - 1)))) {
|
||||
rv = selCont->CharacterExtendForBackspace();
|
||||
nsresult rv = selCont->CharacterExtendForBackspace();
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
return NS_OK;
|
||||
}
|
||||
case eToBeginningOfLine:
|
||||
selCont->IntraLineMove(true, false); // try to move to end
|
||||
rv = selCont->IntraLineMove(false, true); // select to beginning
|
||||
case eToBeginningOfLine: {
|
||||
// Try to move to end
|
||||
selCont->IntraLineMove(true, false);
|
||||
// Select to beginning
|
||||
nsresult rv = selCont->IntraLineMove(false, true);
|
||||
*aAction = eNone;
|
||||
break;
|
||||
case eToEndOfLine:
|
||||
rv = selCont->IntraLineMove(true, true);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
case eToEndOfLine: {
|
||||
nsresult rv = selCont->IntraLineMove(true, true);
|
||||
*aAction = eNext;
|
||||
break;
|
||||
default: // avoid several compiler warnings
|
||||
rv = NS_OK;
|
||||
break;
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
// For avoiding several compiler warnings
|
||||
default:
|
||||
return NS_OK;
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
@ -708,19 +734,28 @@ TextEditor::InsertLineBreak()
|
||||
TextRulesInfo ruleInfo(EditAction::insertBreak);
|
||||
ruleInfo.maxLength = mMaxTextLength;
|
||||
bool cancel, handled;
|
||||
// XXX DidDoAction() won't be called when this returns error. Perhaps,
|
||||
// we should move the code between WillDoAction() and DidDoAction()
|
||||
// to a new method and guarantee that DidDoAction() is always called
|
||||
// after WillDoAction().
|
||||
nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
if (!cancel && !handled) {
|
||||
// get the (collapsed) selection location
|
||||
NS_ENSURE_STATE(selection->GetRangeAt(0));
|
||||
nsCOMPtr<nsINode> selNode = selection->GetRangeAt(0)->GetStartContainer();
|
||||
nsCOMPtr<nsIContent> selChild = selection->GetRangeAt(0)->GetChildAtStartOffset();
|
||||
int32_t selOffset = selection->GetRangeAt(0)->StartOffset();
|
||||
NS_ENSURE_STATE(selNode);
|
||||
nsRange* firstRange = selection->GetRangeAt(0);
|
||||
if (NS_WARN_IF(!firstRange)) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
EditorRawDOMPoint pointToInsert(firstRange->StartRef());
|
||||
if (NS_WARN_IF(!pointToInsert.IsSet())) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
MOZ_ASSERT(pointToInsert.IsSetAndValid());
|
||||
|
||||
// don't put text in places that can't have it
|
||||
if (!IsTextNode(selNode) && !CanContainTag(*selNode,
|
||||
*nsGkAtoms::textTagName)) {
|
||||
if (!IsTextNode(pointToInsert.Container()) &&
|
||||
!CanContainTag(*pointToInsert.Container(), *nsGkAtoms::textTagName)) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
@ -732,29 +767,26 @@ TextEditor::InsertLineBreak()
|
||||
AutoTransactionsConserveSelection dontChangeMySelection(this);
|
||||
|
||||
// insert a linefeed character
|
||||
rv = InsertTextImpl(NS_LITERAL_STRING("\n"), address_of(selNode),
|
||||
address_of(selChild), &selOffset, doc);
|
||||
if (!selNode) {
|
||||
EditorRawDOMPoint pointAfterInsertedLineBreak;
|
||||
rv = InsertTextImpl(*doc, NS_LITERAL_STRING("\n"), pointToInsert,
|
||||
&pointAfterInsertedLineBreak);
|
||||
if (NS_WARN_IF(!pointAfterInsertedLineBreak.IsSet())) {
|
||||
rv = NS_ERROR_NULL_POINTER; // don't return here, so DidDoAction is called
|
||||
}
|
||||
if (NS_SUCCEEDED(rv)) {
|
||||
// set the selection to the correct location
|
||||
MOZ_ASSERT(!selChild,
|
||||
"After inserting text into a text node, selChild should be nullptr");
|
||||
rv = selection->Collapse(EditorRawDOMPoint(selNode, selOffset));
|
||||
MOZ_ASSERT(!pointAfterInsertedLineBreak.GetChildAtOffset(),
|
||||
"After inserting text into a text node, pointAfterInsertedLineBreak."
|
||||
"GetChildAtOffset() should be nullptr");
|
||||
rv = selection->Collapse(pointAfterInsertedLineBreak);
|
||||
if (NS_SUCCEEDED(rv)) {
|
||||
// see if we're at the end of the editor range
|
||||
nsCOMPtr<nsIDOMNode> endNode;
|
||||
int32_t endOffset;
|
||||
rv = GetEndNodeAndOffset(selection,
|
||||
getter_AddRefs(endNode), &endOffset);
|
||||
|
||||
if (NS_SUCCEEDED(rv) &&
|
||||
endNode == GetAsDOMNode(selNode) && endOffset == selOffset) {
|
||||
// SetInterlinePosition(true) means we want the caret to stick to the content on the "right".
|
||||
// We want the caret to stick to whatever is past the break. This is
|
||||
// because the break is on the same line we were on, but the next content
|
||||
// will be on the following line.
|
||||
EditorRawDOMPoint endPoint = GetEndPoint(selection);
|
||||
if (endPoint == pointAfterInsertedLineBreak) {
|
||||
// SetInterlinePosition(true) means we want the caret to stick to the
|
||||
// content on the "right". We want the caret to stick to whatever is
|
||||
// past the break. This is because the break is on the same line we
|
||||
// were on, but the next content will be on the following line.
|
||||
selection->SetInterlinePosition(true);
|
||||
}
|
||||
}
|
||||
|
@ -233,11 +233,10 @@ WSRunObject::InsertBreak(nsCOMPtr<nsINode>* aInOutParent,
|
||||
}
|
||||
|
||||
nsresult
|
||||
WSRunObject::InsertText(const nsAString& aStringToInsert,
|
||||
nsCOMPtr<nsINode>* aInOutParent,
|
||||
nsCOMPtr<nsIContent>* aInOutChildAtOffset,
|
||||
int32_t* aInOutOffset,
|
||||
nsIDocument* aDoc)
|
||||
WSRunObject::InsertText(nsIDocument& aDocument,
|
||||
const nsAString& aStringToInsert,
|
||||
const EditorRawDOMPoint& aPointToInsert,
|
||||
EditorRawDOMPoint* aPointAfterInsertedString)
|
||||
{
|
||||
// MOOSE: for now, we always assume non-PRE formatting. Fix this later.
|
||||
// meanwhile, the pre case is handled in WillInsertText in
|
||||
@ -247,25 +246,33 @@ WSRunObject::InsertText(const nsAString& aStringToInsert,
|
||||
// is very slow. Will need to replace edit rules impl with a more efficient
|
||||
// text sink here that does the minimal amount of searching/replacing/copying
|
||||
|
||||
NS_ENSURE_TRUE(aInOutParent && aInOutOffset && aDoc, NS_ERROR_NULL_POINTER);
|
||||
if (NS_WARN_IF(!aPointToInsert.IsSet())) {
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
MOZ_ASSERT(aPointToInsert.IsSet());
|
||||
|
||||
|
||||
if (aStringToInsert.IsEmpty()) {
|
||||
if (aPointAfterInsertedString) {
|
||||
*aPointAfterInsertedString = aPointToInsert;
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
EditorDOMPoint pointToInsert(aPointToInsert);
|
||||
nsAutoString theString(aStringToInsert);
|
||||
|
||||
WSFragment *beforeRun, *afterRun;
|
||||
FindRun(*aInOutParent, *aInOutOffset, &beforeRun, false);
|
||||
FindRun(*aInOutParent, *aInOutOffset, &afterRun, true);
|
||||
FindRun(pointToInsert.Container(), pointToInsert.Offset(),
|
||||
&beforeRun, false);
|
||||
FindRun(pointToInsert.Container(), pointToInsert.Offset(),
|
||||
&afterRun, true);
|
||||
|
||||
{
|
||||
// Some scoping for AutoTrackDOMPoint. This will track our insertion
|
||||
// point while we tweak any surrounding whitespace
|
||||
AutoTrackDOMPoint tracker(mHTMLEditor->mRangeUpdater, aInOutParent,
|
||||
aInOutOffset);
|
||||
AutoTrackDOMPoint tracker(mHTMLEditor->mRangeUpdater, &pointToInsert);
|
||||
|
||||
bool maybeModified = false;
|
||||
// Handle any changes needed to ws run after inserted text
|
||||
if (!afterRun || afterRun->mType & WSType::trailingWS) {
|
||||
// Don't need to do anything. Just insert text. ws won't change.
|
||||
@ -273,16 +280,15 @@ WSRunObject::InsertText(const nsAString& aStringToInsert,
|
||||
// Delete the leading ws that is after insertion point, because it
|
||||
// would become significant after text inserted.
|
||||
nsresult rv =
|
||||
DeleteChars(*aInOutParent, *aInOutOffset, afterRun->mEndNode,
|
||||
afterRun->mEndOffset);
|
||||
DeleteChars(pointToInsert.Container(), pointToInsert.Offset(),
|
||||
afterRun->mEndNode, afterRun->mEndOffset);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
maybeModified = true;
|
||||
} else if (afterRun->mType == WSType::normalWS) {
|
||||
// Try to change an nbsp to a space, if possible, just to prevent nbsp
|
||||
// proliferation
|
||||
nsresult rv = CheckLeadingNBSP(afterRun, *aInOutParent, *aInOutOffset);
|
||||
nsresult rv = CheckLeadingNBSP(afterRun, pointToInsert.Container(),
|
||||
pointToInsert.Offset());
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
maybeModified = true;
|
||||
}
|
||||
|
||||
// Handle any changes needed to ws run before inserted text
|
||||
@ -293,30 +299,17 @@ WSRunObject::InsertText(const nsAString& aStringToInsert,
|
||||
// it would become significant after text inserted.
|
||||
nsresult rv =
|
||||
DeleteChars(beforeRun->mStartNode, beforeRun->mStartOffset,
|
||||
*aInOutParent, *aInOutOffset);
|
||||
pointToInsert.Container(), pointToInsert.Offset());
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
maybeModified = true;
|
||||
} else if (beforeRun->mType == WSType::normalWS) {
|
||||
// Try to change an nbsp to a space, if possible, just to prevent nbsp
|
||||
// proliferation
|
||||
nsresult rv = CheckTrailingNBSP(beforeRun, *aInOutParent, *aInOutOffset);
|
||||
nsresult rv = CheckTrailingNBSP(beforeRun, pointToInsert.Container(),
|
||||
pointToInsert.Offset());
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
maybeModified = true;
|
||||
}
|
||||
|
||||
// The child node may be changed. So, even though getting child at offset
|
||||
// is expensive, we need to do it here.
|
||||
if (maybeModified) {
|
||||
if ((*aInOutParent)->HasChildren()) {
|
||||
if (*aInOutOffset == 0) {
|
||||
*aInOutChildAtOffset = (*aInOutParent)->GetFirstChild();
|
||||
} else {
|
||||
*aInOutChildAtOffset = (*aInOutParent)->GetChildAt(*aInOutOffset);
|
||||
}
|
||||
} else {
|
||||
*aInOutChildAtOffset = nullptr;
|
||||
}
|
||||
}
|
||||
// After this block, pointToInsert is modified by AutoTrackDOMPoint.
|
||||
}
|
||||
|
||||
// Next up, tweak head and tail of string as needed. First the head: there
|
||||
@ -329,7 +322,8 @@ WSRunObject::InsertText(const nsAString& aStringToInsert,
|
||||
if (beforeRun->mType & WSType::leadingWS) {
|
||||
theString.SetCharAt(nbsp, 0);
|
||||
} else if (beforeRun->mType & WSType::normalWS) {
|
||||
WSPoint wspoint = GetCharBefore(*aInOutParent, *aInOutOffset);
|
||||
WSPoint wspoint =
|
||||
GetCharBefore(pointToInsert.Container(), pointToInsert.Offset());
|
||||
if (wspoint.mTextNode && nsCRT::IsAsciiSpace(wspoint.mChar)) {
|
||||
theString.SetCharAt(nbsp, 0);
|
||||
}
|
||||
@ -348,7 +342,8 @@ WSRunObject::InsertText(const nsAString& aStringToInsert,
|
||||
if (afterRun->mType & WSType::trailingWS) {
|
||||
theString.SetCharAt(nbsp, lastCharIndex);
|
||||
} else if (afterRun->mType & WSType::normalWS) {
|
||||
WSPoint wspoint = GetCharAfter(*aInOutParent, *aInOutOffset);
|
||||
WSPoint wspoint =
|
||||
GetCharAfter(pointToInsert.Container(), pointToInsert.Offset());
|
||||
if (wspoint.mTextNode && nsCRT::IsAsciiSpace(wspoint.mChar)) {
|
||||
theString.SetCharAt(nbsp, lastCharIndex);
|
||||
}
|
||||
@ -377,8 +372,12 @@ WSRunObject::InsertText(const nsAString& aStringToInsert,
|
||||
}
|
||||
|
||||
// Ready, aim, fire!
|
||||
mHTMLEditor->InsertTextImpl(theString, aInOutParent, aInOutChildAtOffset,
|
||||
aInOutOffset, aDoc);
|
||||
nsresult rv =
|
||||
mHTMLEditor->InsertTextImpl(aDocument, theString, pointToInsert.AsRaw(),
|
||||
aPointAfterInsertedString);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return NS_OK;
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
|
@ -219,14 +219,31 @@ public:
|
||||
int32_t* aInOutOffset,
|
||||
nsIEditor::EDirection aSelect);
|
||||
|
||||
// InsertText inserts a string at {aInOutParent,aInOutOffset} and makes any
|
||||
// needed adjustments to ws around that point. Example of fixup:
|
||||
// trailingws before {aInOutParent,aInOutOffset} needs to be removed.
|
||||
nsresult InsertText(const nsAString& aStringToInsert,
|
||||
nsCOMPtr<nsINode>* aInOutNode,
|
||||
nsCOMPtr<nsIContent>* aInOutChildAtOffset,
|
||||
int32_t* aInOutOffset,
|
||||
nsIDocument* aDoc);
|
||||
/**
|
||||
* InsertTextImpl() inserts aStringToInsert to aPointToInsert and makes any
|
||||
* needed adjustments to white spaces around that point. E.g., trailing white
|
||||
* spaces before aPointToInsert needs to be removed.
|
||||
* This calls EditorBase::InsertTextImpl() after adjusting white spaces.
|
||||
* So, please refer the method's explanation to know what this method exactly
|
||||
* does.
|
||||
*
|
||||
* @param aDocument The document of this editor.
|
||||
* @param aStringToInsert The string to insert.
|
||||
* @param aPointToInser The point to insert aStringToInsert.
|
||||
* Must be valid DOM point.
|
||||
* @param aPointAfterInsertedString
|
||||
* The point after inserted aStringToInsert.
|
||||
* So, when this method actually inserts string,
|
||||
* this is set to a point in the text node.
|
||||
* Otherwise, this may be set to aPointToInsert.
|
||||
* @return When this succeeds to insert the string or
|
||||
* does nothing during composition, returns NS_OK.
|
||||
* Otherwise, an error code.
|
||||
*/
|
||||
nsresult InsertText(nsIDocument& aDocument,
|
||||
const nsAString& aStringToInsert,
|
||||
const EditorRawDOMPoint& aPointToInsert,
|
||||
EditorRawDOMPoint* aPointAfterInsertedString = nullptr);
|
||||
|
||||
// DeleteWSBackward deletes a single visible piece of ws before the ws
|
||||
// point (the point to create the wsRunObject, passed to its constructor).
|
||||
|
17
editor/libeditor/crashtests/1415231.html
Normal file
17
editor/libeditor/crashtests/1415231.html
Normal file
@ -0,0 +1,17 @@
|
||||
<script>
|
||||
function jsfuzzer() {
|
||||
var var00043 = window.getSelection();
|
||||
htmlvar00018.addEventListener("DOMNodeRemoved", eh1);
|
||||
var00043.setPosition(htmlvar00016);
|
||||
document.execCommand("insertText", false, "1");
|
||||
}
|
||||
function eh1() {
|
||||
document.execCommand("insertHorizontalRule", false);
|
||||
document.execCommand("justifyCenter", false);
|
||||
}
|
||||
</script>
|
||||
<body onload=jsfuzzer()>
|
||||
<li contenteditable="true">
|
||||
<shadow id="htmlvar00016">
|
||||
<ins id="htmlvar00018">
|
||||
<option>
|
@ -94,3 +94,4 @@ load 1402904.html
|
||||
load 1405747.html
|
||||
load 1408170.html
|
||||
load 1414581.html
|
||||
load 1415231.html
|
||||
|
@ -175,4 +175,4 @@ Troubleshooting tips:
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
The version of WebRender currently in the tree is:
|
||||
34f1e8ed19a19cb950deef89ee31c1cf3d442d22
|
||||
f58ed651b47f47382b63dd2bce6e4ed10ee18c78
|
||||
|
@ -20,7 +20,12 @@ namespace mozilla {
|
||||
struct RoundedRect {
|
||||
typedef mozilla::gfx::RectCornerRadii RectCornerRadii;
|
||||
|
||||
RoundedRect(gfxRect &aRect, RectCornerRadii &aCorners) : rect(aRect), corners(aCorners) { }
|
||||
RoundedRect(const gfxRect& aRect, const RectCornerRadii& aCorners)
|
||||
: rect(aRect)
|
||||
, corners(aCorners)
|
||||
{
|
||||
}
|
||||
|
||||
void Deflate(gfxFloat aTopWidth, gfxFloat aBottomWidth, gfxFloat aLeftWidth, gfxFloat aRightWidth) {
|
||||
// deflate the internal rect
|
||||
rect.x += aLeftWidth;
|
||||
|
@ -93,7 +93,7 @@ protected:
|
||||
|
||||
VRDisplayInfo mDisplayInfo;
|
||||
|
||||
nsTArray<RefPtr<VRLayerParent>> mLayers;
|
||||
nsTArray<VRLayerParent *> mLayers;
|
||||
// Weak reference to mLayers entries are cleared in
|
||||
// VRLayerParent destructor
|
||||
|
||||
|
@ -37,7 +37,7 @@ enum class OculusControllerAxisType : uint16_t {
|
||||
|
||||
class VROculusSession
|
||||
{
|
||||
NS_INLINE_DECL_REFCOUNTING(VROculusSession);
|
||||
NS_INLINE_DECL_THREADSAFE_REFCOUNTING(VROculusSession);
|
||||
friend class VRDisplayOculus;
|
||||
public:
|
||||
VROculusSession();
|
||||
|
@ -370,21 +370,24 @@ VRDisplayPuppet::SubmitFrame(ID3D11Texture2D* aSource,
|
||||
// access the image library. So, we have to convert the RAW image data
|
||||
// to a base64 string and forward it to let the content process to
|
||||
// do the image conversion.
|
||||
char* srcData = static_cast<char*>(mapInfo.pData);
|
||||
const char* srcData = static_cast<const char*>(mapInfo.pData);
|
||||
VRSubmitFrameResultInfo result;
|
||||
result.mFormat = SurfaceFormat::B8G8R8A8;
|
||||
// If the original texture size is not pow of 2, CopyResource() will add padding,
|
||||
// so the size is adjusted. We have to get the correct size by (mapInfo.RowPitch /
|
||||
// the format size).
|
||||
result.mWidth = mapInfo.RowPitch / 4;
|
||||
result.mWidth = desc.Width;
|
||||
result.mHeight = desc.Height;
|
||||
result.mFrameNum = mDisplayInfo.mFrameId;
|
||||
nsCString rawString(Substring(srcData, mapInfo.RowPitch * desc.Height));
|
||||
// If the original texture size is not pow of 2, the data will not be tightly strided.
|
||||
// We have to copy the pixels by rows.
|
||||
nsCString rawString;
|
||||
for (uint32_t i = 0; i < desc.Height; i++) {
|
||||
rawString += Substring(srcData + i * mapInfo.RowPitch,
|
||||
desc.Width * 4);
|
||||
}
|
||||
mContext->Unmap(mappedTexture, 0);
|
||||
|
||||
if (Base64Encode(rawString, result.mBase64Image) != NS_OK) {
|
||||
MOZ_ASSERT(false, "Failed to encode base64 images.");
|
||||
}
|
||||
mContext->Unmap(mappedTexture, 0);
|
||||
// Dispatch the base64 encoded string to the DOM side. Then, it will be decoded
|
||||
// and convert to a PNG image there.
|
||||
MessageLoop* loop = VRListenerThreadHolder::Loop();
|
||||
|
@ -17,7 +17,7 @@ query = []
|
||||
app_units = "0.5.6"
|
||||
bincode = "0.9"
|
||||
byteorder = "1.0"
|
||||
euclid = "0.15.2"
|
||||
euclid = "0.15.5"
|
||||
fxhash = "0.2.1"
|
||||
gleam = "0.4.8"
|
||||
lazy_static = "0.2"
|
||||
|
@ -293,12 +293,44 @@ and output `text_color.a * mask` from our fragment shader.
|
||||
But then there's still the problem that the first summand of the computation for `result.r` uses
|
||||
`text_color.r * mask.r` and the second summand uses `text_color.a * mask.r`.
|
||||
|
||||
There's no way around it, we have to use two passes.
|
||||
(Actually, there is a way around it, but it requires the use of `glBlendColor`, which we want to avoid because
|
||||
we'd have to use different draw calls for different text colors, or it requires "dual source blending" which is
|
||||
not supported everywhere.)
|
||||
There are multiple ways to deal with this. They are:
|
||||
|
||||
Here's how we can express the subpixel text blend function with two passes:
|
||||
1. Making use of `glBlendColor` and the `GL_CONSTANT_COLOR` blend func.
|
||||
2. Using a two-pass method.
|
||||
3. Using "dual source blending".
|
||||
|
||||
Let's look at them in order.
|
||||
|
||||
#### 1. Subpixel text blending in OpenGL using `glBlendColor`
|
||||
|
||||
In this approach we return `text_color.a * mask` from the shader.
|
||||
Then we set the blend color to `text_color / text_color.a` and use `GL_CONSTANT_COLOR` as the source blendfunc.
|
||||
This results in the following blend equation:
|
||||
|
||||
```
|
||||
result.r = (text_color.r / text_color.a) * oFragColor.r + (1 - oFragColor.r) * dest.r;
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^ ^^^^^^^^^^^^^^^^^
|
||||
| | |
|
||||
+--gl::CONSTANT_COLOR | +-- gl::ONE_MINUS_SRC_COLOR
|
||||
|
|
||||
+-- gl::FUNC_ADD
|
||||
|
||||
= (text_color.r / text_color.a) * (text_color.a * mask.r) + (1 - (text_color.a * mask.r)) * dest.r
|
||||
= text_color.r * mask.r + (1 - text_color.a * mask.r) * dest.r
|
||||
```
|
||||
|
||||
At the very beginning of this document, we defined `text_color` as the *premultiplied* text color.
|
||||
So instead of actually doing the calculation `text_color.r / text_color.a` when specifying the blend color,
|
||||
we really just want to use the *unpremultiplied* text color in that place.
|
||||
That's usually the representation we start with anyway.
|
||||
|
||||
#### 2. Two-pass subpixel blending in OpenGL
|
||||
|
||||
The `glBlendColor` method has the disadvantage that the text color is part of the OpenGL state.
|
||||
So if we want to draw text with different colors, we have two use separate batches / draw calls
|
||||
to draw the differently-colored parts of text.
|
||||
|
||||
Alternatively, we can use a two-pass method which avoids the need to use the `GL_CONSTANT_COLOR` blend func:
|
||||
|
||||
- The first pass outputs `text_color.a * mask` from the fragment shader and
|
||||
uses `gl::ZERO, gl::ONE_MINUS_SRC_COLOR` as the glBlendFuncs. This achieves:
|
||||
@ -316,7 +348,7 @@ result_after_pass0.g = 0 * oFragColor.g + (1 - oFragColor.g) * dest.r
|
||||
```
|
||||
|
||||
- The second pass outputs `text_color * mask` from the fragment shader and uses
|
||||
`gl::ONE, gl::ONE` as the glBlendFuncs. This gets us:
|
||||
`gl::ONE, gl::ONE` as the glBlendFuncs. This results in the correct overall blend equation.
|
||||
|
||||
```
|
||||
oFragColor = text_color * mask;
|
||||
@ -327,9 +359,15 @@ result_after_pass1.r
|
||||
= text_color.r * mask.r + (1 - text_color.a * mask.r) * dest.r
|
||||
```
|
||||
|
||||
And analogous results for the other channels.
|
||||
#### 3. Dual source subpixel blending in OpenGL
|
||||
|
||||
This achieves what we set out to do, so we're done here.
|
||||
The third approach is similar to the second approach, but makes use of the [`ARB_blend_func_extended`](https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_blend_func_extended.txt) extension
|
||||
in order to fold the two passes into one:
|
||||
Instead of outputting the two different colors in two separate passes, we output them from the same pass,
|
||||
as two separate fragment shader outputs.
|
||||
Those outputs can then be treated as two different sources in the blend equation.
|
||||
|
||||
This method of text blending has not been implemented in WebRender yet.
|
||||
|
||||
## Subpixel Text Rendering to Transparent Destinations with a Background Color Hint
|
||||
|
||||
|
@ -2,6 +2,15 @@
|
||||
* 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/. */
|
||||
|
||||
//! This example creates a 200x200 white rect and allows the user to move it
|
||||
//! around by using the arrow keys and rotate with '<'/'>'.
|
||||
//! It does this by using the animation API.
|
||||
|
||||
//! The example also features seamless opaque/transparent split of a
|
||||
//! rounded cornered rectangle, which is done automatically during the
|
||||
//! scene building for render optimization.
|
||||
|
||||
extern crate euclid;
|
||||
extern crate gleam;
|
||||
extern crate glutin;
|
||||
extern crate webrender;
|
||||
@ -10,12 +19,11 @@ extern crate webrender;
|
||||
mod boilerplate;
|
||||
|
||||
use boilerplate::{Example, HandyDandyRectBuilder};
|
||||
use euclid::Radians;
|
||||
use webrender::api::*;
|
||||
|
||||
// This example creates a 100x100 white rect and allows the user to move it
|
||||
// around by using the arrow keys. It does this by using the animation API.
|
||||
|
||||
struct App {
|
||||
property_key: PropertyBindingKey<LayoutTransform>,
|
||||
transform: LayoutTransform,
|
||||
}
|
||||
|
||||
@ -29,16 +37,22 @@ impl Example for App {
|
||||
_pipeline_id: PipelineId,
|
||||
_document_id: DocumentId,
|
||||
) {
|
||||
// Create a 100x100 stacking context with an animatable transform property.
|
||||
// Note the magic "42" we use as the animation key. That is used to update
|
||||
// the transform in the keyboard event handler code.
|
||||
let bounds = (0, 0).to(100, 100);
|
||||
let info = LayoutPrimitiveInfo::new(bounds);
|
||||
// Create a 200x200 stacking context with an animated transform property.
|
||||
let bounds = (0, 0).to(200, 200);
|
||||
let complex_clip = ComplexClipRegion {
|
||||
rect: bounds,
|
||||
radii: BorderRadius::uniform(50.0),
|
||||
mode: ClipMode::Clip,
|
||||
};
|
||||
let info = LayoutPrimitiveInfo {
|
||||
local_clip: LocalClip::RoundedRect(bounds, complex_clip),
|
||||
.. LayoutPrimitiveInfo::new(bounds)
|
||||
};
|
||||
|
||||
builder.push_stacking_context(
|
||||
&info,
|
||||
ScrollPolicy::Scrollable,
|
||||
Some(PropertyBinding::Binding(PropertyBindingKey::new(42))),
|
||||
Some(PropertyBinding::Binding(self.property_key)),
|
||||
TransformStyle::Flat,
|
||||
None,
|
||||
MixBlendMode::Normal,
|
||||
@ -54,24 +68,27 @@ impl Example for App {
|
||||
fn on_event(&mut self, event: glutin::Event, api: &RenderApi, document_id: DocumentId) -> bool {
|
||||
match event {
|
||||
glutin::Event::KeyboardInput(glutin::ElementState::Pressed, _, Some(key)) => {
|
||||
let offset = match key {
|
||||
glutin::VirtualKeyCode::Down => (0.0, 10.0),
|
||||
glutin::VirtualKeyCode::Up => (0.0, -10.0),
|
||||
glutin::VirtualKeyCode::Right => (10.0, 0.0),
|
||||
glutin::VirtualKeyCode::Left => (-10.0, 0.0),
|
||||
let (offset_x, offset_y, angle) = match key {
|
||||
glutin::VirtualKeyCode::Down => (0.0, 10.0, 0.0),
|
||||
glutin::VirtualKeyCode::Up => (0.0, -10.0, 0.0),
|
||||
glutin::VirtualKeyCode::Right => (10.0, 0.0, 0.0),
|
||||
glutin::VirtualKeyCode::Left => (-10.0, 0.0, 0.0),
|
||||
glutin::VirtualKeyCode::Comma => (0.0, 0.0, 0.1),
|
||||
glutin::VirtualKeyCode::Period => (0.0, 0.0, -0.1),
|
||||
_ => return false,
|
||||
};
|
||||
// Update the transform based on the keyboard input and push it to
|
||||
// webrender using the generate_frame API. This will recomposite with
|
||||
// the updated transform.
|
||||
let new_transform = self.transform
|
||||
.post_translate(LayoutVector3D::new(offset.0, offset.1, 0.0));
|
||||
.pre_rotate(0.0, 0.0, 1.0, Radians::new(angle))
|
||||
.post_translate(LayoutVector3D::new(offset_x, offset_y, 0.0));
|
||||
api.generate_frame(
|
||||
document_id,
|
||||
Some(DynamicProperties {
|
||||
transforms: vec![
|
||||
PropertyValue {
|
||||
key: PropertyBindingKey::new(42),
|
||||
key: self.property_key,
|
||||
value: new_transform,
|
||||
},
|
||||
],
|
||||
@ -89,7 +106,8 @@ impl Example for App {
|
||||
|
||||
fn main() {
|
||||
let mut app = App {
|
||||
transform: LayoutTransform::identity(),
|
||||
property_key: PropertyBindingKey::new(42), // arbitrary magic number
|
||||
transform: LayoutTransform::create_translation(0.0, 0.0, 0.0),
|
||||
};
|
||||
boilerplate::main_wrapper(&mut app, None);
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user