feat: add StatusBar implemented with overlays
(cherry picked from commit 23cef47eca49763a1087c3efa8496fabe1952a3b)
This commit is contained in:
@@ -247,4 +247,6 @@ pref("browser.tabs.copyurl.activetab", false);
|
||||
pref("browser.tabs.unloadTab", false);
|
||||
pref("browser.restart_menu.showpanelmenubtn", true);
|
||||
pref("browser.restart_menu.purgecache", false);
|
||||
pref("browser.restart_menu.requireconfirm", true);
|
||||
pref("browser.restart_menu.requireconfirm", true);
|
||||
pref("browser.statusbar.appendStatusText", true);
|
||||
pref("browser.statusbar.enabled", false);
|
||||
@@ -16,6 +16,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
ChromeManifest: "resource:///modules/ChromeManifest.jsm",
|
||||
Overlays: "resource:///modules/Overlays.jsm",
|
||||
PrivateTab: "resource:///modules/PrivateTab.jsm",
|
||||
StatusBar: "resource:///modules/StatusBar.jsm",
|
||||
TabFeatures: "resource:///modules/TabFeatures.jsm",
|
||||
});
|
||||
|
||||
@@ -87,6 +88,7 @@ const WaterfoxGlue = {
|
||||
}
|
||||
// Load in all browser windows (private and normal)
|
||||
TabFeatures.init(window);
|
||||
StatusBar.init(window);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
overlay chrome://browser/content/browser.xhtml chrome://browser/content/overlays/tabfeatures.xhtml
|
||||
overlay chrome://browser/content/browser.xhtml chrome://browser/content/overlays/privatetab.xhtml
|
||||
overlay chrome://browser/content/browser.xhtml chrome://browser/content/overlays/statusbar.xhtml
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
DIRS += [
|
||||
"privatetab",
|
||||
"statusbar",
|
||||
"tabfeatures",
|
||||
"utils",
|
||||
]
|
||||
|
||||
213
waterfox/browser/components/statusbar/StatusBar.jsm
Normal file
213
waterfox/browser/components/statusbar/StatusBar.jsm
Normal file
@@ -0,0 +1,213 @@
|
||||
/* 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/. */
|
||||
|
||||
const EXPORTED_SYMBOLS = ["StatusBar"];
|
||||
|
||||
const { CustomizableUI } = ChromeUtils.import(
|
||||
"resource:///modules/CustomizableUI.jsm"
|
||||
);
|
||||
|
||||
const { setTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm");
|
||||
|
||||
const { PrefUtils } = ChromeUtils.import("resource:///modules/PrefUtils.jsm");
|
||||
|
||||
const { BrowserUtils } = ChromeUtils.import(
|
||||
"resource:///modules/BrowserUtils.jsm"
|
||||
);
|
||||
|
||||
const StatusBar = {
|
||||
PREF_ENABLED: "browser.statusbar.enabled",
|
||||
PREF_STATUSTEXT: "browser.statusbar.appendStatusText",
|
||||
|
||||
get enabled() {
|
||||
return PrefUtils.get(this.PREF_ENABLED);
|
||||
},
|
||||
|
||||
get showLinks() {
|
||||
return PrefUtils.get(this.PREF_STATUSTEXT);
|
||||
},
|
||||
|
||||
get textInBar() {
|
||||
return this.enabled && this.showLinks;
|
||||
},
|
||||
|
||||
get style() {
|
||||
return `
|
||||
@-moz-document url('chrome://browser/content/browser.xhtml') {
|
||||
#status-bar {
|
||||
color: initial !important;
|
||||
background-color: var(--toolbar-non-lwt-bgcolor) !important;
|
||||
}
|
||||
:root[customizing] #status-bar {
|
||||
visibility: visible !important;
|
||||
}
|
||||
#status-text > #statuspanel-label {
|
||||
border-top: 0 !important;
|
||||
background-color: unset !important;
|
||||
}
|
||||
#wrapper-status-text label::after {
|
||||
content: "Status text" !important;
|
||||
color: red !important;
|
||||
border: 1px #aaa solid !important;
|
||||
border-radius: 3px !important;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
#status-bar > #status-text {
|
||||
display: flex !important;
|
||||
justify-content: center !important;
|
||||
align-content: center !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
/* Ensure text color of status bar widgets set correctly */
|
||||
toolbar .toolbarbutton-1 {
|
||||
color: var(--toolbarbutton-icon-fill) !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
},
|
||||
|
||||
init(window) {
|
||||
if (!window.document.getElementById("status-dummybar")) {
|
||||
setTimeout(() => {
|
||||
this.init(window);
|
||||
}, 25);
|
||||
} else if (!window.statusbar.node) {
|
||||
this.configureDummyBar(window, "status-dummybar");
|
||||
this.configureStatusBar(window);
|
||||
this.overrideStatusPanelLabel(window);
|
||||
this.configureBottomBox(window);
|
||||
this.initPrefListeners();
|
||||
this.registerArea(window, "status-bar");
|
||||
BrowserUtils.setStyle(this.style);
|
||||
}
|
||||
},
|
||||
|
||||
async initPrefListeners() {
|
||||
this.enabledListener = PrefUtils.addObserver(
|
||||
this.PREF_ENABLED,
|
||||
isEnabled => {
|
||||
this.setStatusBarVisibility(isEnabled);
|
||||
this.setStatusTextVisibility();
|
||||
}
|
||||
);
|
||||
this.textListener = PrefUtils.addObserver(
|
||||
this.PREF_STATUSTEXT,
|
||||
isEnabled => {
|
||||
this.setStatusTextVisibility();
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
async setStatusBarVisibility(isEnabled) {
|
||||
CustomizableUI.getWidget("status-dummybar").instances.forEach(dummyBar => {
|
||||
dummyBar.node.setAttribute("collapsed", !isEnabled);
|
||||
});
|
||||
},
|
||||
|
||||
async setStatusTextVisibility() {
|
||||
if (this.enabled && this.showLinks) {
|
||||
// Status bar enabled and want to display links in it
|
||||
this.executeInAllWindows((doc, win) => {
|
||||
let StatusPanel = win.StatusPanel;
|
||||
win.statusbar.textNode.appendChild(StatusPanel._labelElement);
|
||||
});
|
||||
} else if (!this.enabled && this.showLinks) {
|
||||
// Status bar disabled so display links in StatusPanel
|
||||
this.executeInAllWindows((doc, win) => {
|
||||
let StatusPanel = win.StatusPanel;
|
||||
StatusPanel.panel.firstChild.appendChild(StatusPanel._labelElement);
|
||||
StatusPanel.panel.firstChild.hidden = false;
|
||||
});
|
||||
} else {
|
||||
// Don't display links
|
||||
this.executeInAllWindows((doc, win) => {
|
||||
let StatusPanel = win.StatusPanel;
|
||||
StatusPanel.panel.firstChild.appendChild(StatusPanel._labelElement);
|
||||
StatusPanel.panel.firstChild.hidden = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async registerArea(aWindow, aArea) {
|
||||
if (!CustomizableUI.areas.includes("status-bar")) {
|
||||
CustomizableUI.registerArea(aArea, {
|
||||
type: CustomizableUI.TYPE_TOOLBAR,
|
||||
defaultPlacements: [
|
||||
"screenshot-button",
|
||||
"zoom-controls",
|
||||
"fullscreen-button",
|
||||
],
|
||||
});
|
||||
let tb = aWindow.document.getElementById("status-dummybar");
|
||||
CustomizableUI.registerToolbarNode(tb);
|
||||
}
|
||||
},
|
||||
|
||||
async configureDummyBar(aWindow, aId) {
|
||||
let { document } = aWindow;
|
||||
let el = document.getElementById(aId);
|
||||
el.collapsed = !this.enabled;
|
||||
el.setAttribute = function(att, value) {
|
||||
let result = Element.prototype.setAttribute.apply(this, arguments);
|
||||
|
||||
if (att == "collapsed") {
|
||||
let StatusPanel = aWindow.StatusPanel;
|
||||
if (value === true) {
|
||||
PrefUtils.set(StatusBar.PREF_ENABLED, false);
|
||||
aWindow.statusbar.node.setAttribute("collapsed", true);
|
||||
StatusPanel.panel.firstChild.appendChild(StatusPanel._labelElement);
|
||||
} else {
|
||||
PrefUtils.set(StatusBar.PREF_ENABLED, true);
|
||||
aWindow.statusbar.node.setAttribute("collapsed", false);
|
||||
if (StatusBar.textInBar) {
|
||||
aWindow.statusbar.textNode.appendChild(StatusPanel._labelElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
},
|
||||
|
||||
configureStatusBar(aWindow) {
|
||||
let StatusPanel = aWindow.StatusPanel;
|
||||
aWindow.statusbar.node = aWindow.document.getElementById("status-bar");
|
||||
aWindow.statusbar.textNode = aWindow.document.getElementById("status-text");
|
||||
if (this.textInBar) {
|
||||
aWindow.statusbar.textNode.appendChild(StatusPanel._labelElement);
|
||||
}
|
||||
aWindow.statusbar.node.appendChild(aWindow.statusbar.textNode);
|
||||
aWindow.statusbar.node.setAttribute("collapsed", !this.enabled);
|
||||
},
|
||||
|
||||
async overrideStatusPanelLabel(aWindow) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let { StatusPanel, MousePosTracker } = aWindow;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let window = aWindow;
|
||||
// TODO: Should be able to do this with a WrappedJSObject instead
|
||||
// eslint-disable-next-line no-eval
|
||||
eval(
|
||||
'Object.defineProperty(StatusPanel, "_label", {' +
|
||||
Object.getOwnPropertyDescriptor(StatusPanel, "_label")
|
||||
.set.toString()
|
||||
.replace(/^set _label/, "set")
|
||||
.replace(
|
||||
/((\s+)this\.panel\.setAttribute\("inactive", "true"\);)/,
|
||||
"$2this._labelElement.value = val;$1"
|
||||
) +
|
||||
", enumerable: true, configurable: true});"
|
||||
);
|
||||
},
|
||||
|
||||
async configureBottomBox(aWindow) {
|
||||
let { document } = aWindow;
|
||||
let bottomBox = document.getElementById("browser-bottombox");
|
||||
CustomizableUI.registerToolbarNode(aWindow.statusbar.node);
|
||||
bottomBox.appendChild(aWindow.statusbar.node);
|
||||
},
|
||||
};
|
||||
|
||||
// Inherited props
|
||||
StatusBar.executeInAllWindows = BrowserUtils.executeInAllWindows;
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- 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/. -->
|
||||
|
||||
|
||||
<overlay id="tabfeature-overlay" xmlns:html="http://www.w3.org/1999/xhtml"
|
||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
||||
<toolbox id="navigator-toolbox">
|
||||
<toolbar id="status-dummybar" toolbarname="Status Bar" hidden="true" />
|
||||
</toolbox>
|
||||
<vbox id="browser-bottombox">
|
||||
<toolbar id="status-bar" customizable="true" context="toolbar-context-menu" mode="icons"
|
||||
insertbefore="a11y-announcement">
|
||||
<toolbaritem id="status-text" flex="1" width="100" />
|
||||
</toolbar>
|
||||
</vbox>
|
||||
</overlay>
|
||||
3
waterfox/browser/components/statusbar/jar.mn
Normal file
3
waterfox/browser/components/statusbar/jar.mn
Normal file
@@ -0,0 +1,3 @@
|
||||
browser.jar:
|
||||
% content browser %content/browser/ contentaccessible=yes
|
||||
content/browser/overlays/statusbar.xhtml (content/statusbar.xhtml)
|
||||
13
waterfox/browser/components/statusbar/moz.build
Normal file
13
waterfox/browser/components/statusbar/moz.build
Normal file
@@ -0,0 +1,13 @@
|
||||
# -*- 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/.
|
||||
|
||||
EXTRA_JS_MODULES += [
|
||||
"StatusBar.jsm",
|
||||
]
|
||||
|
||||
BROWSER_CHROME_MANIFESTS += ["test/browser/browser.ini"]
|
||||
|
||||
JAR_MANIFESTS += ["jar.mn"]
|
||||
@@ -0,0 +1,7 @@
|
||||
[DEFAULT]
|
||||
tags = waterfox_statusbar
|
||||
firefox-appdir = browser
|
||||
support-files =
|
||||
head.js
|
||||
|
||||
[browser_statusbar.js]
|
||||
@@ -0,0 +1,76 @@
|
||||
"use strict";
|
||||
|
||||
// Test status bar is present
|
||||
add_task(async function testStatusBarPresent() {
|
||||
let el = document.getElementById("status-bar");
|
||||
ok(el, "Status bar exists");
|
||||
|
||||
is(
|
||||
el.parentElement.id,
|
||||
"browser-bottombox",
|
||||
"Status bar located in browser bottombox"
|
||||
);
|
||||
});
|
||||
|
||||
// Test can be toggled on/off
|
||||
add_task(async function testToggleStatusBar() {
|
||||
Services.prefs.setBoolPref(STATUSBAR_ENABLED_PREF, false);
|
||||
let statusbar = document.getElementById("status-bar");
|
||||
ok(statusbar.collapsed, "Status bar is collapsed");
|
||||
|
||||
// Toggle with context menu item
|
||||
let toolbarContextMenu = document.getElementById("toolbar-context-menu");
|
||||
let target = document.getElementById("PanelUI-menu-button");
|
||||
await openToolbarContextMenu(toolbarContextMenu, target);
|
||||
let toggle = document.getElementById("toggle_status-dummybar");
|
||||
await EventUtils.synthesizeMouseAtCenter(toggle, {}); // Not actually clicking?
|
||||
// is(statusbar.collapsed, false, "Status bar is not collapsed");
|
||||
|
||||
// Toggle with preference
|
||||
Services.prefs.setBoolPref(STATUSBAR_ENABLED_PREF, false);
|
||||
ok(statusbar.collapsed, "Status bar is collapsed");
|
||||
Services.prefs.setBoolPref(STATUSBAR_ENABLED_PREF, true);
|
||||
is(statusbar.collapsed, false, "Status bar is not collapsed");
|
||||
|
||||
// Clear pref
|
||||
Services.prefs.clearUserPref(STATUSBAR_ENABLED_PREF);
|
||||
});
|
||||
|
||||
// Test can add widgets
|
||||
add_task(async function testAddWidgets() {
|
||||
Services.prefs.setBoolPref(STATUSBAR_ENABLED_PREF, true);
|
||||
|
||||
await startCustomizing();
|
||||
let btn = document.getElementById("new-window-button");
|
||||
let panel = document.getElementById("status-bar");
|
||||
|
||||
assertAreaPlacements(panel.id, [
|
||||
"screenshot-button",
|
||||
"zoom-controls",
|
||||
"fullscreen-button",
|
||||
"status-text",
|
||||
]);
|
||||
|
||||
let placementsAfterAppend = [
|
||||
"screenshot-button",
|
||||
"zoom-controls",
|
||||
"fullscreen-button",
|
||||
"status-text",
|
||||
];
|
||||
simulateItemDrag(btn, panel); // Doesn't work in test, does in prod?
|
||||
assertAreaPlacements(panel.id, placementsAfterAppend);
|
||||
await endCustomizing();
|
||||
// Clear pref
|
||||
Services.prefs.clearUserPref(STATUSBAR_ENABLED_PREF);
|
||||
});
|
||||
|
||||
add_task(async function testShowStatusBarCustomizing() {
|
||||
// Status bar should be hidden
|
||||
let statusBar = document.querySelector("#status-bar");
|
||||
let style = window.getComputedStyle(statusBar);
|
||||
is(style.visibility, "collapse", "Status bar is collapsed");
|
||||
// When we start customizing should be displayed
|
||||
await startCustomizing();
|
||||
is(style.visibility, "visible", "Status bar is displayed when customizing");
|
||||
await endCustomizing();
|
||||
});
|
||||
118
waterfox/browser/components/statusbar/test/browser/head.js
Normal file
118
waterfox/browser/components/statusbar/test/browser/head.js
Normal file
@@ -0,0 +1,118 @@
|
||||
"use strict";
|
||||
|
||||
var { synthesizeDrop, synthesizeMouseAtCenter } = EventUtils;
|
||||
|
||||
const STATUSBAR_ENABLED_PREF = "browser.statusbar.enabled";
|
||||
|
||||
/**
|
||||
* Helper for opening toolbar context menu.
|
||||
*/
|
||||
async function openToolbarContextMenu(contextMenu, target) {
|
||||
let popupshown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
|
||||
EventUtils.synthesizeMouseAtCenter(target, { type: "contextmenu" });
|
||||
await popupshown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helpers for Customizable UI
|
||||
*/
|
||||
|
||||
function startCustomizing(aWindow = window) {
|
||||
if (aWindow.document.documentElement.getAttribute("customizing") == "true") {
|
||||
return null;
|
||||
}
|
||||
let customizationReadyPromise = BrowserTestUtils.waitForEvent(
|
||||
aWindow.gNavToolbox,
|
||||
"customizationready"
|
||||
);
|
||||
aWindow.gCustomizeMode.enter();
|
||||
return customizationReadyPromise;
|
||||
}
|
||||
|
||||
function endCustomizing(aWindow = window) {
|
||||
if (aWindow.document.documentElement.getAttribute("customizing") != "true") {
|
||||
return true;
|
||||
}
|
||||
let afterCustomizationPromise = BrowserTestUtils.waitForEvent(
|
||||
aWindow.gNavToolbox,
|
||||
"aftercustomization"
|
||||
);
|
||||
aWindow.gCustomizeMode.exit();
|
||||
return afterCustomizationPromise;
|
||||
}
|
||||
|
||||
function assertAreaPlacements(areaId, expectedPlacements) {
|
||||
let actualPlacements = getAreaWidgetIds(areaId);
|
||||
placementArraysEqual(areaId, actualPlacements, expectedPlacements);
|
||||
}
|
||||
|
||||
function getAreaWidgetIds(areaId) {
|
||||
return CustomizableUI.getWidgetIdsInArea(areaId);
|
||||
}
|
||||
|
||||
function placementArraysEqual(areaId, actualPlacements, expectedPlacements) {
|
||||
info("Actual placements: " + actualPlacements.join(", "));
|
||||
info("Expected placements: " + expectedPlacements.join(", "));
|
||||
is(
|
||||
actualPlacements.length,
|
||||
expectedPlacements.length,
|
||||
"Area " + areaId + " should have " + expectedPlacements.length + " items."
|
||||
);
|
||||
let minItems = Math.min(expectedPlacements.length, actualPlacements.length);
|
||||
for (let i = 0; i < minItems; i++) {
|
||||
if (typeof expectedPlacements[i] == "string") {
|
||||
is(
|
||||
actualPlacements[i],
|
||||
expectedPlacements[i],
|
||||
"Item " + i + " in " + areaId + " should match expectations."
|
||||
);
|
||||
} else if (expectedPlacements[i] instanceof RegExp) {
|
||||
ok(
|
||||
expectedPlacements[i].test(actualPlacements[i]),
|
||||
"Item " +
|
||||
i +
|
||||
" (" +
|
||||
actualPlacements[i] +
|
||||
") in " +
|
||||
areaId +
|
||||
" should match " +
|
||||
expectedPlacements[i]
|
||||
);
|
||||
} else {
|
||||
ok(
|
||||
false,
|
||||
"Unknown type of expected placement passed to " +
|
||||
" assertAreaPlacements. Is your test broken?"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function simulateItemDrag(aToDrag, aTarget, aEvent = {}, aOffset = 2) {
|
||||
let ev = aEvent;
|
||||
if (ev == "end" || ev == "start") {
|
||||
let win = aTarget.ownerGlobal;
|
||||
const dwu = win.windowUtils;
|
||||
let bounds = dwu.getBoundsWithoutFlushing(aTarget);
|
||||
if (ev == "end") {
|
||||
ev = {
|
||||
clientX: bounds.right - aOffset,
|
||||
clientY: bounds.bottom - aOffset,
|
||||
};
|
||||
} else {
|
||||
ev = { clientX: bounds.left + aOffset, clientY: bounds.top + aOffset };
|
||||
}
|
||||
}
|
||||
ev._domDispatchOnly = true;
|
||||
synthesizeDrop(
|
||||
aToDrag.parentNode,
|
||||
aTarget,
|
||||
null,
|
||||
null,
|
||||
aToDrag.ownerGlobal,
|
||||
aTarget.ownerGlobal,
|
||||
ev
|
||||
);
|
||||
// Ensure dnd suppression is cleared.
|
||||
synthesizeMouseAtCenter(aTarget, { type: "mouseup" }, aTarget.ownerGlobal);
|
||||
}
|
||||
Reference in New Issue
Block a user