зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1678390: Prevent Picture-in-Picture windows from opening on top of one another r=mconley
Differential Revision: https://phabricator.services.mozilla.com/D97847
This commit is contained in:
Родитель
2e75f81b2e
Коммит
738d90142b
|
@ -23,6 +23,10 @@ XPCOMUtils.defineLazyServiceGetters(this, {
|
|||
WindowsUIUtils: ["@mozilla.org/windows-ui-utils;1", "nsIWindowsUIUtils"],
|
||||
});
|
||||
|
||||
const { Rect, Point } = ChromeUtils.import(
|
||||
"resource://gre/modules/Geometry.jsm"
|
||||
);
|
||||
|
||||
const PLAYER_URI = "chrome://global/content/pictureinpicture/player.xhtml";
|
||||
var PLAYER_FEATURES =
|
||||
"chrome,titlebar=yes,alwaysontop,lockaspectratio,resizable";
|
||||
|
@ -147,7 +151,7 @@ var PictureInPicture = {
|
|||
* Returns the player window if one exists and if it hasn't yet been closed.
|
||||
*
|
||||
* @param {PictureInPictureParent} pipActorRef
|
||||
* Reference to the calling PictureInPictureParent actor
|
||||
* Reference to the calling PictureInPictureParent actor
|
||||
*
|
||||
* @return {DOM Window} the player window if it exists and is not in the
|
||||
* process of being closed. Returns null otherwise.
|
||||
|
@ -428,7 +432,7 @@ var PictureInPicture = {
|
|||
* The preferred width of the video.
|
||||
*
|
||||
* @param {PictureInPictureParent} actorReference
|
||||
* Reference to the calling PictureInPictureParent
|
||||
* Reference to the calling PictureInPictureParent
|
||||
*
|
||||
* @returns {Promise}
|
||||
* Resolves once the window has opened and loaded the player component.
|
||||
|
@ -436,9 +440,21 @@ var PictureInPicture = {
|
|||
async openPipWindow(parentWin, videoData) {
|
||||
let { top, left, width, height } = this.fitToScreen(parentWin, videoData);
|
||||
|
||||
let { left: resolvedLeft, top: resolvedTop } = this.resolveOverlapConflicts(
|
||||
left,
|
||||
top,
|
||||
width,
|
||||
height
|
||||
);
|
||||
|
||||
top = Math.round(resolvedTop);
|
||||
left = Math.round(resolvedLeft);
|
||||
width = Math.round(width);
|
||||
height = Math.round(height);
|
||||
|
||||
let features =
|
||||
`${PLAYER_FEATURES},top=${Math.round(top)},left=${Math.round(left)},` +
|
||||
`outerWidth=${Math.round(width)},outerHeight=${Math.round(height)}`;
|
||||
`${PLAYER_FEATURES},top=${top},left=${left},outerWidth=${width},` +
|
||||
`outerHeight=${height}`;
|
||||
|
||||
let pipWindow = Services.ww.openWindow(
|
||||
parentWin,
|
||||
|
@ -691,6 +707,157 @@ var PictureInPicture = {
|
|||
return { top, left, width, height };
|
||||
},
|
||||
|
||||
/**
|
||||
* This function will take the size and potential location of a new
|
||||
* Picture-in-Picture player window, and try to return the location
|
||||
* coordinates that will best ensure that the player window will not overlap
|
||||
* with other pre-existing player windows.
|
||||
*
|
||||
* @param {int} left
|
||||
* x position of left edge for Picture-in-Picture window that is being
|
||||
* opened
|
||||
* @param {int} top
|
||||
* y position of top edge for Picture-in-Picture window that is being
|
||||
* opened
|
||||
* @param {int} width
|
||||
* Width of Picture-in-Picture window that is being opened
|
||||
* @param {int} height
|
||||
* Height of Picture-in-Picture window that is being opened
|
||||
*
|
||||
* @returns {object}
|
||||
* An object with the following properties:
|
||||
*
|
||||
* top (int):
|
||||
* The recommended top position for the player window.
|
||||
*
|
||||
* left (int):
|
||||
* The recommended left position for the player window.
|
||||
*/
|
||||
resolveOverlapConflicts(left, top, width, height) {
|
||||
// This algorithm works by first identifying the possible candidate
|
||||
// locations that the new player window could be placed without overlapping
|
||||
// other player windows (assuming that a confict is discovered at all of
|
||||
// course). The optimal candidate is then selected by its distance to the
|
||||
// original conflict, shorter distances are better.
|
||||
//
|
||||
// Candidates are discovered by iterating over each of the sides of every
|
||||
// pre-existing player window. One candidate is collected for each side.
|
||||
// This is done to ensure that the new player window will be opened to
|
||||
// tightly fit along the edge of another player window.
|
||||
//
|
||||
// These candidates are then pruned for candidates that will introduce
|
||||
// further conflicts. Finally the ideal candidate is selected from this
|
||||
// pool of remaining candidates, optimized for minimizing distance to
|
||||
// the original conflict.
|
||||
let playerRects = [];
|
||||
|
||||
for (let playerWin of Services.wm.getEnumerator(WINDOW_TYPE)) {
|
||||
playerRects.push(
|
||||
new Rect(
|
||||
playerWin.screenX,
|
||||
playerWin.screenY,
|
||||
playerWin.outerWidth,
|
||||
playerWin.outerHeight
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const newPlayerRect = new Rect(left, top, width, height);
|
||||
let conflictingPipRect = playerRects.find(rect =>
|
||||
rect.intersects(newPlayerRect)
|
||||
);
|
||||
|
||||
if (!conflictingPipRect) {
|
||||
// no conflicts found
|
||||
return { left, top };
|
||||
}
|
||||
|
||||
const conflictLoc = conflictingPipRect.center();
|
||||
|
||||
// Will try to resolve a better placement only on the screen where
|
||||
// the conflict occurred
|
||||
const conflictScreen = this.getWorkingScreen(conflictLoc.x, conflictLoc.y);
|
||||
|
||||
const [
|
||||
screenTop,
|
||||
screenLeft,
|
||||
screenWidth,
|
||||
screenHeight,
|
||||
] = this.getAvailScreenSize(conflictScreen);
|
||||
|
||||
const screenRect = new Rect(
|
||||
screenTop,
|
||||
screenLeft,
|
||||
screenWidth,
|
||||
screenHeight
|
||||
);
|
||||
|
||||
const getEdgeCandidates = rect => {
|
||||
return [
|
||||
// left edge's candidate
|
||||
new Point(rect.left - newPlayerRect.width, rect.top),
|
||||
// top edge's candidate
|
||||
new Point(rect.left, rect.top - newPlayerRect.height),
|
||||
// right edge's candidate
|
||||
new Point(rect.right + newPlayerRect.width, rect.top),
|
||||
// bottom edge's candidate
|
||||
new Point(rect.left, rect.bottom),
|
||||
];
|
||||
};
|
||||
|
||||
let candidateLocations = [];
|
||||
for (const playerRect of playerRects) {
|
||||
for (let candidateLoc of getEdgeCandidates(playerRect)) {
|
||||
const candidateRect = new Rect(
|
||||
candidateLoc.x,
|
||||
candidateLoc.y,
|
||||
width,
|
||||
height
|
||||
);
|
||||
|
||||
if (!screenRect.contains(candidateRect)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// test that no PiPs conflict with this candidate box
|
||||
if (playerRects.some(rect => rect.intersects(candidateRect))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const candidateCenter = candidateRect.center();
|
||||
const candidateDistanceToConflict =
|
||||
Math.abs(conflictLoc.x - candidateCenter.x) +
|
||||
Math.abs(conflictLoc.y - candidateCenter.y);
|
||||
|
||||
candidateLocations.push({
|
||||
distanceToConflict: candidateDistanceToConflict,
|
||||
location: candidateLoc,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!candidateLocations.length) {
|
||||
// if no suitable candidates can be found, return the original location
|
||||
return { left, top };
|
||||
}
|
||||
|
||||
// sort candidates by distance to the conflict, select the closest
|
||||
const closestCandidate = candidateLocations.sort(
|
||||
(firstCand, secondCand) =>
|
||||
firstCand.distanceToConflict - secondCand.distanceToConflict
|
||||
)[0];
|
||||
|
||||
if (!closestCandidate) {
|
||||
// can occur if there were no valid candidates, return original location
|
||||
return { left, top };
|
||||
}
|
||||
|
||||
const resolvedX = closestCandidate.location.x;
|
||||
const resolvedY = closestCandidate.location.y;
|
||||
|
||||
return { left: resolvedX, top: resolvedY };
|
||||
},
|
||||
|
||||
/**
|
||||
* Resizes the the PictureInPicture player window.
|
||||
*
|
||||
|
|
|
@ -45,6 +45,7 @@ prefs =
|
|||
[browser_closePipPause.js]
|
||||
[browser_contextMenu.js]
|
||||
skip-if = os == "linux" && bits == 64 && os_version == "18.04" # Bug 1569205
|
||||
[browser_conflictingPips.js]
|
||||
[browser_cornerSnapping.js]
|
||||
run-if = os == "mac"
|
||||
[browser_closePlayer.js]
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* If multiple PiPs try to open in the same place, they should not overlap.
|
||||
*/
|
||||
add_task(async () => {
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
url: TEST_PAGE,
|
||||
gBrowser,
|
||||
},
|
||||
async browser => {
|
||||
let firstPip = await triggerPictureInPicture(browser, "with-controls");
|
||||
ok(firstPip, "Got first PiP window");
|
||||
|
||||
await ensureMessageAndClosePiP(browser, "with-controls", firstPip, false);
|
||||
info("Closed first PiP to save location");
|
||||
|
||||
let secondPip = await triggerPictureInPicture(browser, "with-controls");
|
||||
ok(secondPip, "Got second PiP window");
|
||||
|
||||
let thirdPip = await triggerPictureInPicture(browser, "no-controls");
|
||||
ok(thirdPip, "Got third PiP window");
|
||||
|
||||
Assert.ok(
|
||||
secondPip.screenX != thirdPip.screenX ||
|
||||
secondPip.screenY != thirdPip.screenY,
|
||||
"Conflicting PiPs were successfully opened in different locations"
|
||||
);
|
||||
|
||||
await ensureMessageAndClosePiP(
|
||||
browser,
|
||||
"with-controls",
|
||||
secondPip,
|
||||
false
|
||||
);
|
||||
info("Second PiP was still open and is now closed");
|
||||
|
||||
await ensureMessageAndClosePiP(browser, "no-controls", thirdPip, false);
|
||||
info("Third PiP was still open and is now closed");
|
||||
}
|
||||
);
|
||||
});
|
Загрузка…
Ссылка в новой задаче