Bug 1271047 - Place "popup" type window to given coordinates r=mixedpuppy

Differential Revision: https://phabricator.services.mozilla.com/D73419
This commit is contained in:
YUKI "Piro" Hiroshi
2022-11-21 17:02:06 +00:00
parent bd3511e8d0
commit 91ce3fc92a
4 changed files with 377 additions and 1 deletions

View File

@@ -17,6 +17,69 @@ ChromeUtils.defineESModuleGetters(this, {
var { ExtensionError, promiseObserved } = ExtensionUtils;
function sanitizePositionParams(params, window = null, positionOffset = 0) {
if (params.left === null && params.top === null) {
return;
}
if (params.left === null) {
const baseLeft = window ? window.screenX : 0;
params.left = baseLeft + positionOffset;
}
if (params.top === null) {
const baseTop = window ? window.screenY : 0;
params.top = baseTop + positionOffset;
}
// boundary check: don't put window out of visible area
const baseWidth = window ? window.outerWidth : 0;
const baseHeight = window ? window.outerHeight : 0;
// Secure minimum size of an window should be same to the one
// defined at nsGlobalWindowOuter::CheckSecurityWidthAndHeight.
const minWidth = 100;
const minHeight = 100;
const width = Math.max(
minWidth,
params.width !== null ? params.width : baseWidth
);
const height = Math.max(
minHeight,
params.height !== null ? params.height : baseHeight
);
const screenManager = Cc["@mozilla.org/gfx/screenmanager;1"].getService(
Ci.nsIScreenManager
);
const screen = screenManager.screenForRect(
params.left,
params.top,
width,
height
);
const availDeviceLeft = {};
const availDeviceTop = {};
const availDeviceWidth = {};
const availDeviceHeight = {};
screen.GetAvailRect(
availDeviceLeft,
availDeviceTop,
availDeviceWidth,
availDeviceHeight
);
const factor = screen.defaultCSSScaleFactor;
const availLeft = Math.floor(availDeviceLeft.value / factor);
const availTop = Math.floor(availDeviceTop.value / factor);
const availWidth = Math.floor(availDeviceWidth.value / factor);
const availHeight = Math.floor(availDeviceHeight.value / factor);
params.left = Math.min(
availLeft + availWidth - width,
Math.max(availLeft, params.left)
);
params.top = Math.min(
availTop + availHeight - height,
Math.max(availTop, params.top)
);
}
this.windows = class extends ExtensionAPIPersistent {
windowEventRegistrar(event, listener) {
let { extension } = this;
@@ -325,10 +388,12 @@ this.windows = class extends ExtensionAPIPersistent {
"dialog",
"resizable",
"minimizable",
"centerscreen",
"titlebar",
"close"
);
if (createData.left === null && createData.top === null) {
features.push("centerscreen");
}
}
if (createData.incognito !== null) {
@@ -344,6 +409,10 @@ this.windows = class extends ExtensionAPIPersistent {
}
}
const baseWindow = windowTracker.getTopNormalWindow(context);
// 10px offset is same to Chromium
sanitizePositionParams(createData, baseWindow, 10);
let window = Services.ww.openWindow(
null,
AppConstants.BROWSER_CHROME_URL,
@@ -427,6 +496,7 @@ this.windows = class extends ExtensionAPIPersistent {
win.window.getAttention();
}
sanitizePositionParams(updateInfo, win.window);
win.updateGeometry(updateInfo);
if (updateInfo.titlePreface !== null) {

View File

@@ -124,3 +124,125 @@ add_task(async function testWindowCreateFocused() {
ExtensionTestUtils.failOnSchemaWarnings(true);
});
add_task(async function testPopupTypeWithDimension() {
let extension = ExtensionTestUtils.loadExtension({
async background() {
await browser.windows.create({
type: "popup",
left: 123,
top: 123,
width: 151,
height: 152,
});
await browser.windows.create({
type: "popup",
left: 123,
width: 152,
height: 153,
});
await browser.windows.create({
type: "popup",
top: 123,
width: 153,
height: 154,
});
await browser.windows.create({
type: "popup",
left: screen.availWidth * 100,
top: screen.availHeight * 100,
width: 154,
height: 155,
});
await browser.windows.create({
type: "popup",
left: -screen.availWidth * 100,
top: -screen.availHeight * 100,
width: 155,
height: 156,
});
browser.test.sendMessage("windows-created");
},
});
const baseWindow = await BrowserTestUtils.openNewBrowserWindow();
baseWindow.resizeTo(150, 150);
baseWindow.moveTo(50, 50);
let windows = [];
let windowListener = (window, topic) => {
if (topic == "domwindowopened") {
windows.push(window);
}
};
Services.ww.registerNotification(windowListener);
await extension.startup();
await extension.awaitMessage("windows-created");
await extension.unload();
const regularScreen = getScreenAt(0, 0, 150, 150);
const roundedX = roundCssPixcel(123, regularScreen);
const roundedY = roundCssPixcel(123, regularScreen);
const availRectLarge = getCssAvailRect(
getScreenAt(screen.width * 100, screen.height * 100, 150, 150)
);
const maxRight = availRectLarge.right;
const maxBottom = availRectLarge.bottom;
const availRectSmall = getCssAvailRect(
getScreenAt(-screen.width * 100, -screen.height * 100, 150, 150150)
);
const minLeft = availRectSmall.left;
const minTop = availRectSmall.top;
const actualCoordinates = windows
.slice(0, 3)
.map(window => `${window.screenX},${window.screenY}`);
const offsetFromBase = 10;
const expectedCoordinates = [
`${roundedX},${roundedY}`,
// Missing top should be +10 from the last browser window.
`${roundedX},${baseWindow.screenY + offsetFromBase}`,
// Missing left should be +10 from the last browser window.
`${baseWindow.screenX + offsetFromBase},${roundedY}`,
];
is(
actualCoordinates.join(" / "),
expectedCoordinates.join(" / "),
"expected popup type windows are opened at given coordinates"
);
const actualSizes = windows
.slice(0, 3)
.map(window => `${window.outerWidth}x${window.outerHeight}`);
const expectedSizes = [`151x152`, `152x153`, `153x154`];
is(
actualSizes.join(" / "),
expectedSizes.join(" / "),
"expected popup type windows are opened with given size"
);
const actualRect = {
top: windows[4].screenY,
bottom: windows[3].screenY + windows[3].outerHeight,
left: windows[4].screenX,
right: windows[3].screenX + windows[3].outerWidth,
};
const maxRect = {
top: minTop,
bottom: maxBottom,
left: minLeft,
right: maxRight,
};
isRectContained(actualRect, maxRect);
for (const window of windows) {
window.close();
}
Services.ww.unregisterNotification(windowListener);
windows = null;
await BrowserTestUtils.closeWindow(baseWindow);
});

View File

@@ -229,3 +229,136 @@ add_task(async function testWindowUpdateParams() {
await extension.awaitFinish("window-update-params");
await extension.unload();
});
add_task(async function testPositionBoundaryCheck() {
const extension = ExtensionTestUtils.loadExtension({
async background() {
function waitMessage() {
return new Promise((resolve, reject) => {
const onMessage = message => {
if (message == "continue") {
browser.test.onMessage.removeListener(onMessage);
resolve();
}
};
browser.test.onMessage.addListener(onMessage);
});
}
const win = await browser.windows.create({
type: "popup",
left: 50,
top: 50,
width: 150,
height: 150,
});
await browser.test.sendMessage("ready");
await waitMessage();
await browser.windows.update(win.id, {
left: 123,
top: 123,
});
await browser.test.sendMessage("regular");
await waitMessage();
await browser.windows.update(win.id, {
left: 123,
});
await browser.test.sendMessage("only-left");
await waitMessage();
await browser.windows.update(win.id, {
top: 123,
});
await browser.test.sendMessage("only-top");
await waitMessage();
await browser.windows.update(win.id, {
left: screen.availWidth * 100,
top: screen.availHeight * 100,
});
await browser.test.sendMessage("too-large");
await waitMessage();
await browser.windows.update(win.id, {
left: -screen.availWidth * 100,
top: -screen.availHeight * 100,
});
await browser.test.sendMessage("too-small");
},
});
const promisedWin = new Promise((resolve, reject) => {
const windowListener = (window, topic) => {
if (topic == "domwindowopened") {
Services.ww.unregisterNotification(windowListener);
resolve(window);
}
};
Services.ww.registerNotification(windowListener);
});
await extension.startup();
const win = await promisedWin;
const regularScreen = getScreenAt(0, 0, 150, 150);
const roundedX = roundCssPixcel(123, regularScreen);
const roundedY = roundCssPixcel(123, regularScreen);
const availRectLarge = getCssAvailRect(
getScreenAt(screen.width * 100, screen.height * 100, 150, 150)
);
const maxRight = availRectLarge.right;
const maxBottom = availRectLarge.bottom;
const availRectSmall = getCssAvailRect(
getScreenAt(-screen.width * 100, -screen.height * 100, 150, 150)
);
const minLeft = availRectSmall.left;
const minTop = availRectSmall.top;
const expectedCoordinates = [
`${roundedX},${roundedY}`,
`${roundedX},${win.screenY}`,
`${win.screenX},${roundedY}`,
];
await extension.awaitMessage("ready");
const actualCoordinates = [];
extension.sendMessage("continue");
await extension.awaitMessage("regular");
actualCoordinates.push(`${win.screenX},${win.screenY}`);
win.moveTo(50, 50);
extension.sendMessage("continue");
await extension.awaitMessage("only-left");
actualCoordinates.push(`${win.screenX},${win.screenY}`);
win.moveTo(50, 50);
extension.sendMessage("continue");
await extension.awaitMessage("only-top");
actualCoordinates.push(`${win.screenX},${win.screenY}`);
is(
actualCoordinates.join(" / "),
expectedCoordinates.join(" / "),
"expected window is placed at given coordinates"
);
const actualRect = {};
const maxRect = {
top: minTop,
bottom: maxBottom,
left: minLeft,
right: maxRight,
};
extension.sendMessage("continue");
await extension.awaitMessage("too-large");
actualRect.right = win.screenX + win.outerWidth;
actualRect.bottom = win.screenY + win.outerHeight;
extension.sendMessage("continue");
await extension.awaitMessage("too-small");
actualRect.top = win.screenY;
actualRect.left = win.screenX;
isRectContained(actualRect, maxRect);
await extension.unload();
await BrowserTestUtils.closeWindow(win);
});

View File

@@ -29,6 +29,7 @@
* navigateTab historyPushState promiseWindowRestored
* getIncognitoWindow startIncognitoMonitorExtension
* loadTestSubscript awaitBrowserLoaded backgroundColorSetOnRoot
* getScreenAt roundCssPixcel getCssAvailRect isRectContained
*/
// There are shutdown issues for which multiple rejections are left uncaught.
@@ -1011,3 +1012,53 @@ function backgroundColorSetOnRoot() {
}
return os.windowsVersion < 10;
}
function getScreenAt(left, top, width, height) {
const screenManager = Cc["@mozilla.org/gfx/screenmanager;1"].getService(
Ci.nsIScreenManager
);
return screenManager.screenForRect(left, top, width, height);
}
function roundCssPixcel(pixel, screen) {
return Math.floor(
Math.floor(pixel * screen.defaultCSSScaleFactor) /
screen.defaultCSSScaleFactor
);
}
function getCssAvailRect(screen) {
const availDeviceLeft = {};
const availDeviceTop = {};
const availDeviceWidth = {};
const availDeviceHeight = {};
screen.GetAvailRect(
availDeviceLeft,
availDeviceTop,
availDeviceWidth,
availDeviceHeight
);
const factor = screen.defaultCSSScaleFactor;
const left = Math.floor(availDeviceLeft.value / factor);
const top = Math.floor(availDeviceTop.value / factor);
const width = Math.floor(availDeviceWidth.value / factor);
const height = Math.floor(availDeviceHeight.value / factor);
return {
left,
top,
width,
height,
right: left + width,
bottom: top + height,
};
}
function isRectContained(actualRect, maxRect) {
is(
`top=${actualRect.top >= maxRect.top},bottom=${actualRect.bottom <=
maxRect.bottom},left=${actualRect.left >=
maxRect.left},right=${actualRect.right <= maxRect.right}`,
"top=true,bottom=true,left=true,right=true",
`Dimension must be inside, top:${actualRect.top}>=${maxRect.top}, bottom:${actualRect.bottom}<=${maxRect.bottom}, left:${actualRect.left}>=${maxRect.left}, right:${actualRect.right}<=${maxRect.right}`
);
}