diff options
Diffstat (limited to '')
-rw-r--r-- | toolkit/actors/UAWidgetsChild.sys.mjs | 236 |
1 files changed, 236 insertions, 0 deletions
diff --git a/toolkit/actors/UAWidgetsChild.sys.mjs b/toolkit/actors/UAWidgetsChild.sys.mjs new file mode 100644 index 0000000000..6f4244ffe9 --- /dev/null +++ b/toolkit/actors/UAWidgetsChild.sys.mjs @@ -0,0 +1,236 @@ +/* vim: set ts=2 sw=2 sts=2 et tw=80: */ +/* 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/. */ + +export class UAWidgetsChild extends JSWindowActorChild { + constructor() { + super(); + + this.widgets = new WeakMap(); + this.prefsCache = new Map(); + this.observedPrefs = []; + + // Bug 1570744 - JSWindowActorChild's cannot be used as nsIObserver's + // directly, so we create a new function here instead to act as our + // nsIObserver, which forwards the notification to the observe method. + this.observerFunction = (subject, topic, data) => { + this.observe(subject, topic, data); + }; + } + + didDestroy() { + for (let pref in this.observedPrefs) { + Services.prefs.removeObserver(pref, this.observerFunction); + } + } + + unwrap(obj) { + return Cu.isXrayWrapper(obj) ? obj.wrappedJSObject : obj; + } + + handleEvent(aEvent) { + switch (aEvent.type) { + case "UAWidgetSetupOrChange": + this.setupOrNotifyWidget(aEvent.target); + break; + case "UAWidgetTeardown": + this.teardownWidget(aEvent.target); + break; + } + } + + setupOrNotifyWidget(aElement) { + if (!this.widgets.has(aElement)) { + this.setupWidget(aElement); + return; + } + + let { widget } = this.widgets.get(aElement); + + if (typeof widget.onchange == "function") { + if ( + this.unwrap(aElement.openOrClosedShadowRoot) != + this.unwrap(widget.shadowRoot) + ) { + console.error( + "Getting a UAWidgetSetupOrChange event without the ShadowRoot. " + + "Torn down already?" + ); + return; + } + try { + widget.onchange(); + } catch (ex) { + console.error(ex); + } + } + } + + setupWidget(aElement) { + let uri; + let widgetName; + // Use prefKeys to optionally send a list of preferences to forward to + // the UAWidget. The UAWidget will receive those preferences as key-value + // pairs as the second argument to its constructor. Updates to those prefs + // can be observed by implementing an optional onPrefChange method for the + // UAWidget that receives the changed pref name as the first argument, and + // the updated value as the second. + let prefKeys = []; + switch (aElement.localName) { + case "video": + case "audio": + uri = "chrome://global/content/elements/videocontrols.js"; + widgetName = "VideoControlsWidget"; + prefKeys = [ + "media.videocontrols.picture-in-picture.enabled", + "media.videocontrols.picture-in-picture.video-toggle.enabled", + "media.videocontrols.picture-in-picture.video-toggle.always-show", + "media.videocontrols.picture-in-picture.video-toggle.min-video-secs", + "media.videocontrols.picture-in-picture.video-toggle.position", + "media.videocontrols.picture-in-picture.video-toggle.has-used", + "media.videocontrols.keyboard-tab-to-all-controls", + "media.videocontrols.picture-in-picture.respect-disablePictureInPicture", + ]; + break; + case "input": + uri = "chrome://global/content/elements/datetimebox.js"; + widgetName = "DateTimeBoxWidget"; + break; + case "marquee": + uri = "chrome://global/content/elements/marquee.js"; + widgetName = "MarqueeWidget"; + break; + case "img": + uri = "chrome://global/content/elements/textrecognition.js"; + widgetName = "TextRecognitionWidget"; + } + + if (!uri || !widgetName) { + console.error( + "Getting a UAWidgetSetupOrChange event on undefined element." + ); + return; + } + + let shadowRoot = aElement.openOrClosedShadowRoot; + if (!shadowRoot) { + console.error( + "Getting a UAWidgetSetupOrChange event without the Shadow Root. " + + "Torn down already?" + ); + return; + } + + let isSystemPrincipal = aElement.nodePrincipal.isSystemPrincipal; + let sandbox = isSystemPrincipal + ? Object.create(null) + : Cu.getUAWidgetScope(aElement.nodePrincipal); + + if (!sandbox[widgetName]) { + Services.scriptloader.loadSubScript(uri, sandbox); + } + + let prefs = Cu.cloneInto( + this.getPrefsForUAWidget(widgetName, prefKeys), + sandbox + ); + + let widget = new sandbox[widgetName](shadowRoot, prefs); + if (!isSystemPrincipal) { + widget = widget.wrappedJSObject; + } + if (this.unwrap(widget.shadowRoot) != this.unwrap(shadowRoot)) { + console.error("Widgets should expose their shadow root."); + } + this.widgets.set(aElement, { widget, widgetName }); + try { + widget.onsetup(); + } catch (ex) { + console.error(ex); + } + } + + teardownWidget(aElement) { + if (!this.widgets.has(aElement)) { + return; + } + let { widget } = this.widgets.get(aElement); + if (typeof widget.teardown == "function") { + try { + widget.teardown(); + } catch (ex) { + console.error(ex); + } + } + this.widgets.delete(aElement); + } + + getPrefsForUAWidget(aWidgetName, aPrefKeys) { + let result = this.prefsCache.get(aWidgetName); + if (result) { + return result; + } + + result = {}; + for (let key of aPrefKeys) { + result[key] = this.getPref(key); + this.observePref(key); + } + + this.prefsCache.set(aWidgetName, result); + return result; + } + + observePref(prefKey) { + Services.prefs.addObserver(prefKey, this.observerFunction); + this.observedPrefs.push(prefKey); + } + + getPref(prefKey) { + switch (Services.prefs.getPrefType(prefKey)) { + case Ci.nsIPrefBranch.PREF_BOOL: { + return Services.prefs.getBoolPref(prefKey); + } + case Ci.nsIPrefBranch.PREF_INT: { + return Services.prefs.getIntPref(prefKey); + } + case Ci.nsIPrefBranch.PREF_STRING: { + return Services.prefs.getStringPref(prefKey); + } + } + + return undefined; + } + + observe(subject, topic, data) { + if (topic == "nsPref:changed") { + for (let [widgetName, prefCache] of this.prefsCache) { + if (prefCache.hasOwnProperty(data)) { + let newValue = this.getPref(data); + prefCache[data] = newValue; + + this.notifyWidgetsOnPrefChange(widgetName, data, newValue); + } + } + } + } + + notifyWidgetsOnPrefChange(nameOfWidgetToNotify, prefKey, newValue) { + let elements = ChromeUtils.nondeterministicGetWeakMapKeys(this.widgets); + for (let element of elements) { + if (!Cu.isDeadWrapper(element) && element.isConnected) { + let { widgetName, widget } = this.widgets.get(element); + if (widgetName == nameOfWidgetToNotify) { + if (typeof widget.onPrefChange == "function") { + try { + widget.onPrefChange(prefKey, newValue); + } catch (ex) { + console.error(ex); + } + } + } + } + } + } +} |