diff options
Diffstat (limited to 'toolkit/mozapps/handling/content/appChooser.js')
-rw-r--r-- | toolkit/mozapps/handling/content/appChooser.js | 386 |
1 files changed, 386 insertions, 0 deletions
diff --git a/toolkit/mozapps/handling/content/appChooser.js b/toolkit/mozapps/handling/content/appChooser.js new file mode 100644 index 0000000000..328b4b8822 --- /dev/null +++ b/toolkit/mozapps/handling/content/appChooser.js @@ -0,0 +1,386 @@ +/* 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 { PrivateBrowsingUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PrivateBrowsingUtils.sys.mjs" +); +const { EnableDelayHelper } = ChromeUtils.import( + "resource://gre/modules/SharedPromptUtils.jsm" +); + +class MozHandler extends window.MozElements.MozRichlistitem { + static get markup() { + return ` + <vbox pack="center"> + <image height="32" width="32"/> + </vbox> + <vbox flex="1"> + <label class="name"/> + <label class="description"/> + </vbox> + `; + } + + connectedCallback() { + this.textContent = ""; + this.appendChild(this.constructor.fragment); + this.initializeAttributeInheritance(); + } + + static get inheritedAttributes() { + return { + image: "src=image,disabled", + ".name": "value=name,disabled", + ".description": "value=description,disabled", + }; + } + + get label() { + return `${this.getAttribute("name")} ${this.getAttribute("description")}`; + } +} + +customElements.define("mozapps-handler", MozHandler, { + extends: "richlistitem", +}); + +window.addEventListener("DOMContentLoaded", () => dialog.initialize(), { + once: true, +}); + +let loadPromise = new Promise(resolve => { + window.addEventListener("load", resolve, { once: true }); +}); + +let dialog = { + /** + * This function initializes the content of the dialog. + */ + initialize() { + let args = window.arguments[0].wrappedJSObject || window.arguments[0]; + let { handler, outArgs, usePrivateBrowsing, enableButtonDelay } = args; + + this._handlerInfo = handler.QueryInterface(Ci.nsIHandlerInfo); + this._outArgs = outArgs; + + this.isPrivate = + usePrivateBrowsing || + (window.opener && PrivateBrowsingUtils.isWindowPrivate(window.opener)); + + this._dialog = document.querySelector("dialog"); + this._itemChoose = document.getElementById("item-choose"); + this._rememberCheck = document.getElementById("remember"); + + // Register event listener for the checkbox hint. + this._rememberCheck.addEventListener("change", () => this.onCheck()); + + document.addEventListener("dialogaccept", () => { + this.onAccept(); + }); + + // UI is ready, lets populate our list + this.populateList(); + + document.mozSubdialogReady = this.initL10n().then(() => { + window.sizeToContent(); + }); + + if (enableButtonDelay) { + this._delayHelper = new EnableDelayHelper({ + disableDialog: () => { + this._acceptBtnDisabled = true; + this.updateAcceptButton(); + }, + enableDialog: () => { + this._acceptBtnDisabled = false; + this.updateAcceptButton(); + }, + focusTarget: window, + }); + } + }, + + async initL10n() { + document.l10n.pauseObserving(); + + let rememberLabel = document.getElementById("remember-label"); + document.l10n.setAttributes(rememberLabel, "chooser-dialog-remember", { + scheme: this._handlerInfo.type, + }); + + let description = document.getElementById("description"); + document.l10n.setAttributes(description, "chooser-dialog-description", { + scheme: this._handlerInfo.type, + }); + + document.l10n.resumeObserving(); + + await document.l10n.translateElements([rememberLabel, description]); + return document.l10n.ready; + }, + + /** + * Populates the list that a user can choose from. + */ + populateList: function populateList() { + var items = document.getElementById("items"); + var possibleHandlers = this._handlerInfo.possibleApplicationHandlers; + var preferredHandler = this._handlerInfo.preferredApplicationHandler; + for (let i = possibleHandlers.length - 1; i >= 0; --i) { + let app = possibleHandlers.queryElementAt(i, Ci.nsIHandlerApp); + let elm = document.createXULElement("richlistitem", { + is: "mozapps-handler", + }); + elm.setAttribute("name", app.name); + elm.obj = app; + + // We defer loading the favicon so it doesn't delay load. The dialog is + // opened in a SubDialog which will only show on window load. + if (app instanceof Ci.nsILocalHandlerApp) { + // See if we have an nsILocalHandlerApp and set the icon + let uri = Services.io.newFileURI(app.executable); + loadPromise.then(() => { + elm.setAttribute("image", "moz-icon://" + uri.spec + "?size=32"); + }); + } else if (app instanceof Ci.nsIWebHandlerApp) { + let uri = Services.io.newURI(app.uriTemplate); + if (/^https?$/.test(uri.scheme)) { + // Unfortunately we can't use the favicon service to get the favicon, + // because the service looks for a record with the exact URL we give + // it, and users won't have such records for URLs they don't visit, + // and users won't visit the handler's URL template, they'll only + // visit URLs derived from that template (i.e. with %s in the template + // replaced by the URL of the content being handled). + loadPromise.then(() => { + elm.setAttribute("image", uri.prePath + "/favicon.ico"); + }); + } + elm.setAttribute("description", uri.prePath); + + // Check for extensions needing private browsing access before + // creating UI elements. + if (this.isPrivate) { + let policy = WebExtensionPolicy.getByURI(uri); + if (policy && !policy.privateBrowsingAllowed) { + elm.setAttribute("disabled", true); + this.getPrivateBrowsingDisabledLabel().then(label => { + elm.setAttribute("description", label); + }); + if (app == preferredHandler) { + preferredHandler = null; + } + } + } + } else if (app instanceof Ci.nsIDBusHandlerApp) { + elm.setAttribute("description", app.method); + } else if (!(app instanceof Ci.nsIGIOMimeApp)) { + // We support GIO application handler, but no action required there + throw new Error("unknown handler type"); + } + + items.insertBefore(elm, this._itemChoose); + if (preferredHandler && app == preferredHandler) { + this.selectedItem = elm; + } + } + + if (this._handlerInfo.hasDefaultHandler) { + let elm = document.createXULElement("richlistitem", { + is: "mozapps-handler", + }); + elm.id = "os-default-handler"; + elm.setAttribute("name", this._handlerInfo.defaultDescription); + + items.insertBefore(elm, items.firstChild); + if ( + this._handlerInfo.preferredAction == Ci.nsIHandlerInfo.useSystemDefault + ) { + this.selectedItem = elm; + } + } + + // Add gio handlers + if (Cc["@mozilla.org/gio-service;1"]) { + let gIOSvc = Cc["@mozilla.org/gio-service;1"].getService( + Ci.nsIGIOService + ); + var gioApps = gIOSvc.getAppsForURIScheme(this._handlerInfo.type); + for (let handler of gioApps.enumerate(Ci.nsIHandlerApp)) { + // OS handler share the same name, it's most likely the same app, skipping... + if (handler.name == this._handlerInfo.defaultDescription) { + continue; + } + // Check if the handler is already in possibleHandlers + let appAlreadyInHandlers = false; + for (let i = possibleHandlers.length - 1; i >= 0; --i) { + let app = possibleHandlers.queryElementAt(i, Ci.nsIHandlerApp); + // nsGIOMimeApp::Equals is able to compare with nsILocalHandlerApp + if (handler.equals(app)) { + appAlreadyInHandlers = true; + break; + } + } + if (!appAlreadyInHandlers) { + let elm = document.createXULElement("richlistitem", { + is: "mozapps-handler", + }); + elm.setAttribute("name", handler.name); + elm.obj = handler; + items.insertBefore(elm, this._itemChoose); + } + } + } + + items.ensureSelectedElementIsVisible(); + }, + + /** + * Brings up a filepicker and allows a user to choose an application. + */ + async chooseApplication() { + let title = await this.getChooseAppWindowTitle(); + + var fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + fp.init(window, title, Ci.nsIFilePicker.modeOpen); + fp.appendFilters(Ci.nsIFilePicker.filterApps); + + fp.open(rv => { + if (rv == Ci.nsIFilePicker.returnOK && fp.file) { + let uri = Services.io.newFileURI(fp.file); + + let handlerApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + handlerApp.executable = fp.file; + + // if this application is already in the list, select it and don't add it again + let parent = document.getElementById("items"); + for (let i = 0; i < parent.childNodes.length; ++i) { + let elm = parent.childNodes[i]; + if ( + elm.obj instanceof Ci.nsILocalHandlerApp && + elm.obj.equals(handlerApp) + ) { + parent.selectedItem = elm; + parent.ensureSelectedElementIsVisible(); + return; + } + } + + let elm = document.createXULElement("richlistitem", { + is: "mozapps-handler", + }); + elm.setAttribute("name", fp.file.leafName); + elm.setAttribute("image", "moz-icon://" + uri.spec + "?size=32"); + elm.obj = handlerApp; + + parent.selectedItem = parent.insertBefore(elm, parent.firstChild); + parent.ensureSelectedElementIsVisible(); + } + }); + }, + + /** + * Function called when the OK button is pressed. + */ + onAccept() { + this.updateHandlerData(this._rememberCheck.checked); + this._outArgs.setProperty("openHandler", true); + }, + + /** + * Determines if the accept button should be disabled or not + */ + updateAcceptButton() { + this._dialog.setAttribute( + "buttondisabledaccept", + this._acceptBtnDisabled || this._itemChoose.selected + ); + }, + + /** + * Update the handler info to reflect the user choice. + * @param {boolean} skipAsk - Whether we should persist the application + * choice and skip asking next time. + */ + updateHandlerData(skipAsk) { + // We need to make sure that the default is properly set now + if (this.selectedItem.obj) { + // default OS handler doesn't have this property + this._outArgs.setProperty( + "preferredAction", + Ci.nsIHandlerInfo.useHelperApp + ); + this._outArgs.setProperty( + "preferredApplicationHandler", + this.selectedItem.obj + ); + } else { + this._outArgs.setProperty( + "preferredAction", + Ci.nsIHandlerInfo.useSystemDefault + ); + } + this._outArgs.setProperty("alwaysAskBeforeHandling", !skipAsk); + }, + + /** + * Updates the UI based on the checkbox being checked or not. + */ + onCheck() { + if (document.getElementById("remember").checked) { + document.getElementById("remember-text").setAttribute("visible", "true"); + } else { + document.getElementById("remember-text").removeAttribute("visible"); + } + }, + + /** + * Function called when the user double clicks on an item of the list + */ + onDblClick: function onDblClick() { + if (this.selectedItem == this._itemChoose) { + this.chooseApplication(); + } else { + this._dialog.acceptDialog(); + } + }, + + // Getters / Setters + + /** + * Returns/sets the selected element in the richlistbox + */ + get selectedItem() { + return document.getElementById("items").selectedItem; + }, + set selectedItem(aItem) { + document.getElementById("items").selectedItem = aItem; + }, + + /** + * Lazy l10n getter for the title of the app chooser window + */ + async getChooseAppWindowTitle() { + if (!this._chooseAppWindowTitle) { + this._chooseAppWindowTitle = await document.l10n.formatValues([ + "choose-other-app-window-title", + ]); + } + return this._chooseAppWindowTitle; + }, + + /** + * Lazy l10n getter for handler menu items which are disabled due to private + * browsing. + */ + async getPrivateBrowsingDisabledLabel() { + if (!this._privateBrowsingDisabledLabel) { + this._privateBrowsingDisabledLabel = await document.l10n.formatValues([ + "choose-dialog-privatebrowsing-disabled", + ]); + } + return this._privateBrowsingDisabledLabel; + }, +}; |