Commit 70df5bbf authored by Harry Twyford's avatar Harry Twyford
Browse files

Bug 1313429 - Add Touch Bar functionality to Firefox r=spohl,mikedeboer,flod

Adds Touch Bar functionality to Firefox across eight commits.

Differential Revision: https://phabricator.services.mozilla.com/D5496

--HG--
extra : moz-landing-system : lando
parent e09d523f
...@@ -1488,8 +1488,10 @@ var BookmarkingUI = { ...@@ -1488,8 +1488,10 @@ var BookmarkingUI = {
} }
if (starred) { if (starred) {
element.setAttribute("starred", "true"); element.setAttribute("starred", "true");
Services.obs.notifyObservers(null, "bookmark-icon-updated", "starred");
} else { } else {
element.removeAttribute("starred"); element.removeAttribute("starred");
Services.obs.notifyObservers(null, "bookmark-icon-updated", "unstarred");
} }
} }
......
...@@ -4827,6 +4827,7 @@ var XULBrowserWindow = { ...@@ -4827,6 +4827,7 @@ var XULBrowserWindow = {
CFRPageActions.updatePageActions(gBrowser.selectedBrowser); CFRPageActions.updatePageActions(gBrowser.selectedBrowser);
} }
Services.obs.notifyObservers(null, "touchbar-location-change", location);
UpdateBackForwardCommands(gBrowser.webNavigation); UpdateBackForwardCommands(gBrowser.webNavigation);
ReaderParent.updateReaderButton(gBrowser.selectedBrowser); ReaderParent.updateReaderButton(gBrowser.selectedBrowser);
......
...@@ -48,8 +48,10 @@ var gExceptionPaths = [ ...@@ -48,8 +48,10 @@ var gExceptionPaths = [
// These are not part of the omni.ja file, so we find them only when running // These are not part of the omni.ja file, so we find them only when running
// the test on a non-packaged build. // the test on a non-packaged build.
if (AppConstants.platform == "macosx") if (AppConstants.platform == "macosx") {
gExceptionPaths.push("resource://gre/res/cursors/"); gExceptionPaths.push("resource://gre/res/cursors/");
gExceptionPaths.push("resource://gre/res/touchbar/");
}
var whitelist = [ var whitelist = [
// browser/extensions/pdfjs/content/PdfStreamConverter.jsm // browser/extensions/pdfjs/content/PdfStreamConverter.jsm
......
...@@ -65,6 +65,9 @@ if CONFIG['NIGHTLY_BUILD']: ...@@ -65,6 +65,9 @@ if CONFIG['NIGHTLY_BUILD']:
'payments', 'payments',
] ]
if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'cocoa':
DIRS += ['touchbar']
XPIDL_SOURCES += [ XPIDL_SOURCES += [
'nsIBrowserHandler.idl', 'nsIBrowserHandler.idl',
] ]
......
/* 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/. */
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
Services: "resource://gre/modules/Services.jsm",
AppConstants: "resource://gre/modules/AppConstants.jsm",
Localization: "resource://gre/modules/Localization.jsm",
});
/**
* Executes a XUL command on the top window. Called by the callbacks in each
* TouchBarInput.
* @param {string} commandName
* A XUL command.
* @param {string} telemetryKey
* A string describing the command, sent for telemetry purposes.
* Intended to be shorter and more readable than the XUL command.
*/
function execCommand(commandName, telemetryKey) {
let win = BrowserWindowTracker.getTopWindow();
let command = win.document.getElementById(commandName);
if (command) {
command.doCommand();
}
let telemetry = Services.telemetry.getHistogramById("TOUCHBAR_BUTTON_PRESSES");
telemetry.add(telemetryKey);
}
/**
* Static helper function to convert a hexadecimal string to its integer
* value. Used to convert colours to a format accepted by Apple's NSColor code.
* @param {string} hexString
* A hexadecimal string, optionally beginning with '#'.
*/
function hexToInt(hexString) {
if (!hexString) {
return null;
}
if (hexString.charAt(0) == "#") {
hexString = hexString.slice(1);
}
let val = parseInt(hexString, 16);
return isNaN(val) ? null : val;
}
/**
* An object containing all implemented TouchBarInput objects.
*/
const kBuiltInInputs = {
Back: {
title: "back",
image: "back.pdf",
type: "button",
callback: () => execCommand("Browser:Back", "Back"),
},
Forward: {
title: "forward",
image: "forward.pdf",
type: "button",
callback: () => execCommand("Browser:Forward", "Forward"),
},
Reload: {
title: "reload",
image: "refresh.pdf",
type: "button",
callback: () => execCommand("Browser:Reload", "Reload"),
},
Home: {
title: "home",
image: "home.pdf",
type: "button",
callback: () => execCommand("Browser:Home", "Home"),
},
Fullscreen: {
title: "fullscreen",
image: "fullscreen.pdf",
type: "button",
callback: () => execCommand("View:FullScreen", "Fullscreen"),
},
Find: {
title: "find",
image: "search.pdf",
type: "button",
callback: () => execCommand("cmd_find", "Find"),
},
NewTab: {
title: "new-tab",
image: "new.pdf",
type: "button",
callback: () => execCommand("cmd_newNavigatorTabNoEvent", "NewTab"),
},
Sidebar: {
title: "open-bookmarks-sidebar",
image: "sidebar-left.pdf",
type: "button",
callback: () => execCommand("viewBookmarksSidebar", "Sidebar"),
},
AddBookmark: {
title: "add-bookmark",
image: "bookmark.pdf",
type: "button",
callback: () => execCommand("Browser:AddBookmarkAs", "AddBookmark"),
},
ReaderView: {
title: "reader-view",
image: "reader-mode.pdf",
type: "button",
callback: () => execCommand("View:ReaderView", "ReaderView"),
disabled: true, // Updated when the page is found to be Reader View-able.
},
OpenLocation: {
title: "open-location",
image: "search.pdf",
type: "mainButton",
callback: () => execCommand("Browser:OpenLocation", "OpenLocation"),
},
Focus: {
title: "focus",
image: "private-browsing.pdf",
type: "mainButton",
callback: () => execCommand("cmd_closeWindow", "Focus"),
color: "#8000D7",
context: () => {
let name;
if (PrivateBrowsingUtils.isWindowPrivate(BrowserWindowTracker.getTopWindow())) {
name = "Focus";
}
return name;
},
},
OpenOrFocus: {
// This input is just a forwarder to other inputs.
context: () => {
let name;
if (PrivateBrowsingUtils.isWindowPrivate(BrowserWindowTracker.getTopWindow())) {
name = "Focus";
} else {
name = "OpenLocation";
}
return name;
},
},
// This is a special-case `type: "scrubber"` element.
// Scrubbers are not yet generally implemented.
// See follow-up bug 1502539.
Share: {
title: "share",
type: "scrubber",
image: "share.pdf",
callback: () => execCommand("cmd_share", "Share"),
},
};
const kHelperObservers = new Set(["bookmark-icon-updated", "reader-mode-available",
"touchbar-location-change", "quit-application", "intl:app-locales-changed"]);
/**
* JS-implemented TouchBarHelper class.
* Provides services to the Mac Touch Bar.
*/
class TouchBarHelper {
constructor() {
for (let topic of kHelperObservers) {
Services.obs.addObserver(this, topic);
}
XPCOMUtils.defineLazyPreferenceGetter(this, "_touchBarLayout",
"ui.touchbar.layout", "Back,Reload,OpenOrFocus,AddBookmark,NewTab,Share");
}
destructor() {
for (let topic of kHelperObservers) {
Services.obs.removeObserver(this, topic);
}
}
get activeTitle() {
let tabbrowser = this.window.ownerGlobal.gBrowser;
let activeTitle;
if (tabbrowser) {
activeTitle = tabbrowser.selectedBrowser.contentTitle;
}
return activeTitle;
}
get layout() {
let prefArray = this.storedLayout;
let layoutItems = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
for (let inputName of prefArray) {
if (typeof kBuiltInInputs[inputName].context == "function") {
inputName = kBuiltInInputs[inputName].context();
}
let input = this.getTouchBarInput(inputName);
if (!input) {
continue;
}
layoutItems.appendElement(input);
}
return layoutItems;
}
get window() {
return BrowserWindowTracker.getTopWindow();
}
get bookmarkingUI() {
return this.window.BookmarkingUI;
}
/**
* Returns a string array of the user's Touch Bar layout preference.
* Set by a pref ui.touchbar.layout and returned in an array.
*/
get storedLayout() {
let prefArray = this._touchBarLayout.split(",");
prefArray = prefArray.map(str => str.trim());
// Remove duplicates.
prefArray = Array.from(new Set(prefArray));
// Remove unimplemented/mispelled inputs.
prefArray = prefArray.filter(input =>
Object.keys(kBuiltInInputs).includes(input));
this._storedLayout = prefArray;
return this._storedLayout;
}
getTouchBarInput(inputName) {
if (typeof kBuiltInInputs[inputName].context == "function") {
inputName = kBuiltInInputs[inputName].context();
}
let inputData = kBuiltInInputs[inputName];
if (!inputData) {
return null;
}
let item = new TouchBarInput(inputData);
// Async l10n fills in the localized input labels after the initial load.
this._l10n.formatValue(item.key).then((result) => {
item.title = result;
kBuiltInInputs[inputName].localTitle = result; // Cache result.
// Checking this.window since this callback can fire after all windows are closed.
if (this.window) {
let baseWindow = this.window.docShell.treeOwner.QueryInterface(Ci.nsIBaseWindow);
let updater = Cc["@mozilla.org/widget/touchbarupdater;1"]
.getService(Ci.nsITouchBarUpdater);
updater.updateTouchBarInput(baseWindow, item);
}
});
return item;
}
/**
* Fetches a specific Touch Bar Input by name and updates it on the Touch Bar.
* @param {string} inputName
* A key to a value in the kBuiltInInputs object in this file.
*/
_updateTouchBarInput(inputName) {
let input = this.getTouchBarInput(inputName);
if (!input || !this.window) {
return;
}
let baseWindow = this.window.docShell.treeOwner.QueryInterface(Ci.nsIBaseWindow);
let updater = Cc["@mozilla.org/widget/touchbarupdater;1"]
.getService(Ci.nsITouchBarUpdater);
updater.updateTouchBarInput(baseWindow, input);
}
observe(subject, topic, data) {
switch (topic) {
case "touchbar-location-change":
this.activeUrl = data;
if (data.startsWith("about:reader")) {
kBuiltInInputs.ReaderView.disabled = false;
this._updateTouchBarInput("ReaderView");
} else {
// ReaderView button is disabled on every location change since
// Reader View must determine if the new page can be Reader Viewed.
kBuiltInInputs.ReaderView.disabled = true;
this._updateTouchBarInput("ReaderView");
}
break;
case "bookmark-icon-updated":
data == "starred" ?
kBuiltInInputs.AddBookmark.image = "bookmark-filled.pdf"
: kBuiltInInputs.AddBookmark.image = "bookmark.pdf";
this._updateTouchBarInput("AddBookmark");
break;
case "reader-mode-available":
kBuiltInInputs.ReaderView.disabled = false;
this._updateTouchBarInput("ReaderView");
break;
case "intl:app-locales-changed":
// On locale change, refresh all inputs after loading new localTitle.
for (let inputName of this._storedLayout) {
delete kBuiltInInputs[inputName].localTitle;
this._updateTouchBarInput(inputName);
}
case "quit-application":
this.destructor();
break;
}
}
}
const helperProto = TouchBarHelper.prototype;
helperProto.classDescription = "Services the Mac Touch Bar";
helperProto.classID = Components.ID("{ea109912-3acc-48de-b679-c23b6a122da5}");
helperProto.contractID = "@mozilla.org/widget/touchbarhelper;1";
helperProto.QueryInterface = ChromeUtils.generateQI([Ci.nsITouchBarHelper]);
helperProto._l10n = new Localization(["browser/touchbar/touchbar.ftl"]);
/**
* A representation of a Touch Bar input.
* Uses async update() in lieu of a constructor to accomodate async l10n code.
* @param {object} input
* An object representing a Touch Bar Input.
* Contains listed properties.
* @param {string} input.title
* The lookup key for the button's localized text title.
* @param {string} input.image
* The name of a icon file added to
* /widget/cocoa/resources/touchbar_icons.
* @param {string} input.type
* The type of Touch Bar input represented by the object.
* One of `button`, `mainButton`.
* @param {Function} input.callback
* A callback invoked when a touchbar item is touched.
* @param {string} input.color (optional)
* A string in hex format specifying the button's background color.
* If omitted, the default background color is used.
* @param {bool} input.disabled (optional)
* If `true`, the Touch Bar input is greyed out and inoperable.
*/
class TouchBarInput {
constructor(input) {
this._key = input.title;
this._title = input.hasOwnProperty("localTitle") ? input.localTitle : "";
this._image = input.image;
this._type = input.type;
this._callback = input.callback;
this._color = hexToInt(input.color);
this._disabled = input.hasOwnProperty("disabled") ? input.disabled : false;
}
get key() {
return this._key;
}
get title() {
return this._title;
}
set title(title) {
this._title = title;
}
get image() {
return this._image;
}
set image(image) {
this._image = image;
}
get type() {
return this._type == "" ? "button" : this._type;
}
set type(type) {
this._type = type;
}
get callback() {
return this._callback;
}
set callback(callback) {
this._callback = callback;
}
get color() {
return this._color;
}
set color(color) {
this._color = this.hexToInt(color);
}
get disabled() {
return this._disabled || false;
}
set disabled(disabled) {
this._disabled = disabled;
}
}
const inputProto = TouchBarInput.prototype;
inputProto.classDescription = "Represents an input on the Mac Touch Bar";
inputProto.classID = Components.ID("{77441d17-f29c-49d7-982f-f20a5ab5a900}");
inputProto.contractID = "@mozilla.org/widget/touchbarinput;1";
inputProto.QueryInterface = ChromeUtils.generateQI([Ci.nsITouchBarInput]);
this.NSGetFactory =
XPCOMUtils.generateNSGetFactory([TouchBarHelper, TouchBarInput]);
# 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/. */
component {ea109912-3acc-48de-b679-c23b6a122da5} MacTouchBar.js
contract @mozilla.org/widget/touchbarhelper;1 {ea109912-3acc-48de-b679-c23b6a122da5}
component {77441d17-f29c-49d7-982f-f20a5ab5a900} MacTouchBar.js
contract @mozilla.org/widget/touchbarinput;1 {77441d17-f29c-49d7-982f-f20a5ab5a900}
# 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/.
with Files('**'):
BUG_COMPONENT = ('Core', 'Widget: Cocoa')
BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
EXTRA_COMPONENTS += [
'MacTouchBar.js',
'MacTouchBar.manifest',
]
/* 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";
module.exports = {
"extends": [
"plugin:mozilla/browser-test"
]
}
[DEFAULT]
support-files =
readerModeArticle.html
[browser_touchbar_tests.js]
\ No newline at end of file
/* 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 PREF_NAME = "ui.touchbar.layout";
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "TouchBarHelper",
"@mozilla.org/widget/touchbarhelper;1",
"nsITouchBarHelper");
XPCOMUtils.defineLazyServiceGetter(this, "TouchBarInput",
"@mozilla.org/widget/touchbarinput;1",
"nsITouchBarInput");
function is_element_visible(aElement, aMsg) {
isnot(aElement, null, "Element should not be null when checking visibility");
ok(!BrowserTestUtils.is_hidden(aElement), aMsg);
}
function is_element_hidden(aElement, aMsg) {
isnot(aElement, null, "Element should not be null when checking visibility");
ok(BrowserTestUtils.is_hidden(aElement), aMsg);
}
/**
* Sets the pref to contain errors. .layout should contain only the
* non-erroneous elements.
*/
add_task(async function setWrongPref() {
registerCleanupFunction(function() {
Services.prefs.deleteBranch(PREF_NAME);
});
let wrongValue = "Back, Back, Forwrd, NewTab, Unimplemented,";
let correctValue = ["back", "new-tab"];
let testValue = [];
Services.prefs.setStringPref(PREF_NAME, wrongValue);
let layoutItems = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
layoutItems = TouchBarHelper.layout;
for (let i = 0; i < layoutItems.length; i++) {
let input = layoutItems.queryElementAt(i, Ci.nsITouchBarInput);
testValue.push(input.key);
}
Assert.equal(testValue.toString(),
correctValue.toString(),
"Pref should filter out incorrect inputs.");
});
/**
* Tests if our bookmark button updates with our event.
*/
add_task(async function updateBookmarkButton() {
Services.obs.notifyObservers(null, "bookmark-icon-updated", "starred");
Assert.equal(TouchBarHelper.getTouchBarInput("AddBookmark").image,
"bookmark-filled.pdf",
"AddBookmark image should be filled bookmark after event.");
Services.obs.notifyObservers(null, "bookmark-icon-updated", "unstarred");
Assert.equal(TouchBarHelper.getTouchBarInput("AddBookmark").image,
"bookmark.pdf",
"AddBookmark image should be unfilled bookmark after event.");
});
/**
* Tests if our Reader View button updates when a page can be reader viewed.
*/
add_task(async function updateReaderView() {
const PREF_READERMODE = "reader.parse-on-load.enabled";
await SpecialPowers.pushPrefEnv({set: [[PREF_READERMODE, true]]});
// The page actions reader mode button
var readerButton = document.getElementById("reader-mode-button");
is_element_hidden(readerButton, "Reader Mode button should be hidden.");
Assert.equal(TouchBarHelper.getTouchBarInput("ReaderView").disabled,
true,
"ReaderView Touch Bar button should be disabled by default.");