From 68583710fbba5e686e9809ceba15d7824355e5e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabrice=20Desr=C3=A9?= Date: Fri, 10 Jun 2011 17:02:00 -0400 Subject: [PATCH] Bug 584767 - webapps frontend [r=mfinkle, wjohnston, fabrice] --- mobile/chrome/content/browser-scripts.js | 1 + mobile/chrome/content/browser-ui.js | 18 ++- mobile/chrome/content/browser.js | 30 ++++- mobile/chrome/content/browser.xul | 4 +- mobile/chrome/content/common-ui.js | 126 ++++++++++++++++++++ mobile/chrome/content/webapps.xul | 94 +++++++++++++++ mobile/chrome/jar.mn | 1 + mobile/components/BrowserCLH.js | 55 +++++---- mobile/installer/package-manifest.in | 3 + mobile/locales/en-US/chrome/browser.dtd | 1 + mobile/locales/en-US/chrome/webapps.dtd | 6 + mobile/locales/jar.mn | 1 + mobile/themes/core/browser.css | 19 +++ mobile/themes/core/gingerbread/browser.css | 19 +++ mobile/themes/core/gingerbread/platform.css | 5 +- 15 files changed, 351 insertions(+), 32 deletions(-) create mode 100644 mobile/chrome/content/webapps.xul create mode 100644 mobile/locales/en-US/chrome/webapps.dtd diff --git a/mobile/chrome/content/browser-scripts.js b/mobile/chrome/content/browser-scripts.js index 463ec86621e7..8d3ea12f8c6d 100644 --- a/mobile/chrome/content/browser-scripts.js +++ b/mobile/chrome/content/browser-scripts.js @@ -69,6 +69,7 @@ XPCOMUtils.defineLazyGetter(this, "CommonUI", function() { [ ["FullScreenVideo"], + ["WebappsUI"], ["BadgeHandlers"], ["ContextHelper"], ["SelectionHelper"], diff --git a/mobile/chrome/content/browser-ui.js b/mobile/chrome/content/browser-ui.js index 48be27b270bd..08fab028ffb7 100644 --- a/mobile/chrome/content/browser-ui.js +++ b/mobile/chrome/content/browser-ui.js @@ -1038,6 +1038,21 @@ var BrowserUI = { return this._domWindowClose(browser); break; case "DOMLinkAdded": + // checks for an icon to use for a web app + // priority is : icon < apple-touch-icon + let rel = json.rel.toLowerCase().split(" "); + if ((rel.indexOf("icon") != -1) && !browser.appIcon) { + // We should also use the sizes attribute if available + // see http://www.whatwg.org/specs/web-apps/current-work/multipage/links.html#rel-icon + browser.appIcon = json.href; + } + else if (rel.indexOf("apple-touch-icon") != -1) { + // XXX should we support apple-touch-icon-precomposed ? + // see http://developer.apple.com/safari/library/documentation/appleapplications/reference/safariwebcontent/configuringwebapplications/configuringwebapplications.html + browser.appIcon = json.href; + } + + // Handle favicon changes if (Browser.selectedBrowser == browser) this._updateIcon(Browser.selectedBrowser.mIconURL); break; @@ -1238,7 +1253,8 @@ var BrowserUI = { this.activePanel = RemoteTabsList; break; case "cmd_quit": - GlobalOverlay.goQuitApplication(); + // Only close one window + this._closeOrQuit(); break; case "cmd_close": this._closeOrQuit(); diff --git a/mobile/chrome/content/browser.js b/mobile/chrome/content/browser.js index c9c4cb5a9c14..de0d999df4d7 100644 --- a/mobile/chrome/content/browser.js +++ b/mobile/chrome/content/browser.js @@ -378,13 +378,14 @@ var Browser = { messageManager.addMessageListener("Browser:CertException", this); messageManager.addMessageListener("Browser:BlockedSite", this); - // broadcast a UIReady message so add-ons know we are finished with startup + // Broadcast a UIReady message so add-ons know we are finished with startup let event = document.createEvent("Events"); event.initEvent("UIReady", true, false); window.dispatchEvent(event); - // if we have an opener this was not the first window opened and will not + // If we have an opener this was not the first window opened and will not // receive an initial resize event. instead we fire the resize handler manually + // Bug 610834 if (window.opener) resizeHandler({ target: window }); }, @@ -941,7 +942,7 @@ var Browser = { function visibility(aSidebarRect, aVisibleRect) { let width = aSidebarRect.width; aSidebarRect.restrictTo(aVisibleRect); - return (aSidebarRect.width ? aSidebarRect.width / width : 0); + return (width ? aSidebarRect.width / width : 0); } if (!dx) dx = 0; @@ -1492,6 +1493,7 @@ Browser.WebProgress.prototype = { tab.hostChanged = true; tab.browser.lastLocation = location; tab.browser.userTypedValue = ""; + tab.browser.appIcon = null; #ifdef MOZ_CRASH_REPORTER if (CrashReporter.enabled) @@ -1576,6 +1578,8 @@ Browser.WebProgress.prototype = { }; +const OPEN_APPTAB = 100; // Hack until we get a real API + function nsBrowserAccess() { } nsBrowserAccess.prototype = { @@ -1618,13 +1622,31 @@ nsBrowserAccess.prototype = { tab.closeOnExit = true; browser = tab.browser; BrowserUI.hidePanel(); + } else if (aWhere == OPEN_APPTAB) { + Browser.tabs.forEach(function(aTab) { + if ("appURI" in aTab.browser && aTab.browser.appURI.spec == aURI.spec) { + Browser.selectedTab = aTab; + browser = aTab.browser; + } + }); + + if (!browser) { + // Make a new tab to hold the app + let tab = Browser.addTab("about:blank", true, null, { getAttention: true }); + browser = tab.browser; + browser.appURI = aURI; + } else { + // Just use the existing browser, but return null to keep the system from trying to load the URI again + browser = null; + } + BrowserUI.hidePanel(); } else { // OPEN_CURRENTWINDOW and illegal values browser = Browser.selectedBrowser; } try { let referrer; - if (aURI) { + if (aURI && browser) { if (aOpener) { location = aOpener.location; referrer = Services.io.newURI(location, null, null); diff --git a/mobile/chrome/content/browser.xul b/mobile/chrome/content/browser.xul index d62227e6664f..3ca8f2d302e7 100644 --- a/mobile/chrome/content/browser.xul +++ b/mobile/chrome/content/browser.xul @@ -68,10 +68,9 @@ title="&brandShortName;" #ifdef MOZ_PLATFORM_MAEMO sizemode="fullscreen" -#else +#endif width="480" height="800" -#endif onkeypress="onDebugKeyPress(event);" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml"> @@ -349,6 +348,7 @@ onclick="PageActions.clearPagePermissions(event);"/> + diff --git a/mobile/chrome/content/common-ui.js b/mobile/chrome/content/common-ui.js index a9b2f182b338..c6bae4c9f894 100644 --- a/mobile/chrome/content/common-ui.js +++ b/mobile/chrome/content/common-ui.js @@ -174,6 +174,7 @@ var PageActions = { #endif this.register("pageaction-share", this.updateShare, this); this.register("pageaction-search", BrowserSearch.updatePageSearchEngines, BrowserSearch); + this.register("pageaction-webapps-install", WebappsUI.updateWebappsInstall, WebappsUI); }, handleEvent: function handleEvent(aEvent) { @@ -1658,3 +1659,128 @@ var CharsetMenu = { } }; + +var WebappsUI = { + _dialog: null, + _manifest: null, + + checkBox: function(aEvent) { + let elem = aEvent.originalTarget; + let perm = elem.getAttribute("perm"); + if (this._manifest.capabilities && this._manifest.capabilities.indexOf(perm) != -1) { + if (elem.checked) { + elem.classList.remove("webapps-noperm"); + elem.classList.add("webapps-perm"); + } else { + elem.classList.remove("webapps-perm"); + elem.classList.add("webapps-noperm"); + } + } + }, + + show: function show(aManifest) { + if (!aManifest) { + // Try every way to get an icon + let browser = Browser.selectedBrowser; + let icon = browser.appIcon; + if (!icon) + icon = browser.mIconURL; + if (!icon) + icon = gFaviconService.getFaviconImageForPage(browser.currentURI).spec; + + // Create a simple manifest + aManifest = { + uri: browser.currentURI.spec, + name: browser.contentTitle, + icon: icon + }; + } + + this._manifest = aManifest; + this._dialog = importDialog(window, "chrome://browser/content/webapps.xul", null); + + if (aManifest.name) + document.getElementById("webapps-title").value = aManifest.name; + if (aManifest.icon) + document.getElementById("webapps-icon").src = aManifest.icon; + + let uri = Services.io.newURI(aManifest.uri, null, null); + + let perms = [["offline", "offline-app"], ["geoloc", "geo"], ["notifications", "desktop-notifications"]]; + perms.forEach(function(tuple) { + let elem = document.getElementById("webapps-" + tuple[0] + "-checkbox"); + let currentPerm = Services.perms.testExactPermission(uri, tuple[1]); + if ((aManifest.capabilities && (aManifest.capabilities.indexOf(tuple[1]) != -1)) || (currentPerm == Ci.nsIPermissionManager.ALLOW_ACTION)) + elem.checked = true; + else + elem.checked = (currentPerm == Ci.nsIPermissionManager.ALLOW_ACTION); + elem.classList.remove("webapps-noperm"); + elem.classList.add("webapps-perm"); + }); + + BrowserUI.pushPopup(this, this._dialog); + + // Force a modal dialog + this._dialog.waitForClose(); + }, + + hide: function hide() { + this._dialog.close(); + this._dialog = null; + BrowserUI.popPopup(this); + }, + + _updatePermission: function updatePermission(aId, aPerm) { + try { + let uri = Services.io.newURI(this._manifest.uri, null, null); + Services.perms.add(uri, aPerm, document.getElementById(aId).checked ? Ci.nsIPermissionManager.ALLOW_ACTION : Ci.nsIPermissionManager.DENY_ACTION); + } catch(e) { + Cu.reportError(e); + } + }, + + launch: function launch() { + let title = document.getElementById("webapps-title").value; + if (!title) + return; + + this._updatePermission("webapps-offline-checkbox", "offline-app"); + this._updatePermission("webapps-geoloc-checkbox", "geo"); + this._updatePermission("webapps-notifications-checkbox", "desktop-notification"); + + this.hide(); + this.install(this._manifest.uri, title, this._manifest.icon); + }, + + updateWebappsInstall: function updateWebappsInstall(aNode) { + return !document.getElementById("main-window").hasAttribute("webapp"); + }, + + install: function(aURI, aTitle, aIcon) { + const kIconSize = 64; + + let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); + canvas.setAttribute("style", "display: none"); + + let self = this; + let image = new Image(); + image.onload = function() { + canvas.width = canvas.height = kIconSize; // clears the canvas + let ctx = canvas.getContext("2d"); + ctx.drawImage(image, 0, 0, kIconSize, kIconSize); + let data = canvas.toDataURL("image/png", ""); + canvas = null; + try { + let webapp = Cc["@mozilla.org/webapps/support;1"].getService(Ci.nsIWebappsSupport); + webapp.installApplication(aTitle, aURI, aIcon, data); + } catch(e) { + Cu.reportError(e); + } + } + image.onerror = function() { + // can't load the icon (bad URI) : fallback to the default one from chrome + self.install(aURI, aTitle, "chrome://browser/skin/images/favicon-default-30.png"); + } + image.src = aIcon; + } +}; diff --git a/mobile/chrome/content/webapps.xul b/mobile/chrome/content/webapps.xul new file mode 100644 index 000000000000..9ea62ec0d4cd --- /dev/null +++ b/mobile/chrome/content/webapps.xul @@ -0,0 +1,94 @@ + + + +%promptDTD; + +%webappsDTD; +]> + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/chrome/jar.mn b/mobile/chrome/jar.mn index dbf498988671..853c30582550 100644 --- a/mobile/chrome/jar.mn +++ b/mobile/chrome/jar.mn @@ -64,6 +64,7 @@ chrome.jar: content/prompt/select.xul (content/prompt/select.xul) content/prompt/prompt.js (content/prompt/prompt.js) content/share.xul (content/share.xul) + content/webapps.xul (content/webapps.xul) content/AnimatedZoom.js (content/AnimatedZoom.js) #ifdef MOZ_SERVICES_SYNC content/sync.js (content/sync.js) diff --git a/mobile/components/BrowserCLH.js b/mobile/components/BrowserCLH.js index 6cd37898368e..9c233c3524bb 100644 --- a/mobile/components/BrowserCLH.js +++ b/mobile/components/BrowserCLH.js @@ -44,12 +44,12 @@ Cu.import("resource://gre/modules/Services.jsm"); function openWindow(aParent, aURL, aTarget, aFeatures, aArgs) { let argString = null; - if (aArgs) { + if (aArgs && !(aArgs instanceof Ci.nsISupportsArray)) { argString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); argString.data = aArgs; } - return Services.ww.openWindow(aParent, aURL, aTarget, aFeatures, argString); + return Services.ww.openWindow(aParent, aURL, aTarget, aFeatures, argString || aArgs); } function resolveURIInternal(aCmdLine, aArgument) { @@ -61,16 +61,14 @@ function resolveURIInternal(aCmdLine, aArgument) { try { if (uri.file.exists()) return uri; - } - catch (e) { + } catch (e) { Cu.reportError(e); } try { let urifixup = Cc["@mozilla.org/docshell/urifixup;1"].getService(Ci.nsIURIFixup); uri = urifixup.createFixupURI(aArgument, 0); - } - catch (e) { + } catch (e) { Cu.reportError(e); } @@ -155,8 +153,7 @@ BrowserCLH.prototype = { // Stop the normal commandline processing from continuing aCmdLine.preventDefault = true; } - } - catch (e) { + } catch (e) { Cu.reportError(e); } return; @@ -165,6 +162,12 @@ BrowserCLH.prototype = { // Check and remove the alert flag here, but we'll handle it a bit later - see below let alertFlag = aCmdLine.handleFlagWithParam("alert", false); + // Check and remove the webapp param + let appFlag = aCmdLine.handleFlagWithParam("webapp", false); + let appURI; + if (appFlag) + appURI = resolveURIInternal(aCmdLine, appFlag); + // Keep an array of possible URL arguments let uris = []; @@ -187,16 +190,17 @@ BrowserCLH.prototype = { } // Open the main browser window, if we don't already have one - let win; - let localePickerWin; + let browserWin; try { - win = Services.wm.getMostRecentWindow("navigator:browser"); - localePickerWin = Services.wm.getMostRecentWindow("navigator:localepicker"); - if (localePickerWin) { - localePickerWin.focus(); + let localeWin = Services.wm.getMostRecentWindow("navigator:localepicker"); + if (localeWin) { + localeWin.focus(); aCmdLine.preventDefault = true; return; - } else if (!win) { + } + + browserWin = Services.wm.getMostRecentWindow("navigator:browser"); + if (!browserWin) { // Default to the saved homepage let defaultURL = getHomePage(); @@ -208,30 +212,35 @@ BrowserCLH.prototype = { // Show the locale selector if we have a new profile if (needHomepageOverride() == "new profile" && Services.prefs.getBoolPref("browser.firstrun.show.localepicker")) { - win = openWindow(null, "chrome://browser/content/localePicker.xul", "_blank", "chrome,dialog=no,all", defaultURL); + browserWin = openWindow(null, "chrome://browser/content/localePicker.xul", "_blank", "chrome,dialog=no,all", defaultURL); aCmdLine.preventDefault = true; return; } - win = openWindow(null, "chrome://browser/content/browser.xul", "_blank", "chrome,dialog=no,all", defaultURL); + browserWin = openWindow(null, "chrome://browser/content/browser.xul", "_blank", "chrome,dialog=no,all", defaultURL); } - win.focus(); + browserWin.focus(); // Stop the normal commandline processing from continuing. We just opened the main browser window aCmdLine.preventDefault = true; - } catch (e) { } + } catch (e) { + Cu.reportError(e); + } // Assumption: All remaining command line arguments have been sent remotely (browser is already running) // Action: Open any URLs we find into an existing browser window // First, get a browserDOMWindow object - while (!win.browserDOMWindow) + while (!browserWin.browserDOMWindow) Services.tm.currentThread.processNextEvent(true); // Open any URIs into new tabs for (let i = 0; i < uris.length; i++) - win.browserDOMWindow.openURI(uris[i], null, Ci.nsIBrowserDOMWindow.OPEN_NEWTAB, Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL); + browserWin.browserDOMWindow.openURI(uris[i], null, Ci.nsIBrowserDOMWindow.OPEN_NEWTAB, Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL); + + if (appURI) + browserWin.browserDOMWindow.openURI(appURI, null, browserWin.OPEN_APPTAB, Ci.nsIBrowserDOMWindow.OPEN_NEW); // Handle the notification, if called from it if (alertFlag) { @@ -243,9 +252,9 @@ BrowserCLH.prototype = { var updateTimerCallback = updateService.QueryInterface(Ci.nsITimerCallback); updateTimerCallback.notify(null); } else if (alertFlag.length >= 9 && alertFlag.substr(0, 9) == "download:") { - showPanelWhenReady(win, "downloads-container"); + showPanelWhenReady(browserWin, "downloads-container"); } else if (alertFlag == "addons") { - showPanelWhenReady(win, "addons-container"); + showPanelWhenReady(browserWin, "addons-container"); } } }, diff --git a/mobile/installer/package-manifest.in b/mobile/installer/package-manifest.in index b9c2c67a79b3..b2058ea6bd4b 100644 --- a/mobile/installer/package-manifest.in +++ b/mobile/installer/package-manifest.in @@ -275,6 +275,7 @@ @BINPATH@/components/xuldoc.xpt @BINPATH@/components/xultmpl.xpt @BINPATH@/components/zipwriter.xpt +@BINPATH@/components/webapps.xpt ; JavaScript components @BINPATH@/components/ConsoleAPI.manifest @@ -330,6 +331,7 @@ @BINPATH@/components/amContentHandler.js @BINPATH@/components/amWebInstallListener.js @BINPATH@/components/nsBlocklistService.js + #ifdef MOZ_UPDATER @BINPATH@/components/nsUpdateService.manifest @BINPATH@/components/nsUpdateService.js @@ -610,6 +612,7 @@ bin/components/@DLL_PREFIX@nkgnomevfs@DLL_SUFFIX@ #ifdef MOZ_UPDATER @BINPATH@/components/UpdatePrompt.js #endif +@BINPATH@/components/WebappsSupport.js @BINPATH@/components/XPIDialogService.js @BINPATH@/components/browsercomps.xpt @BINPATH@/extensions/feedback@mobile.mozilla.org.xpi diff --git a/mobile/locales/en-US/chrome/browser.dtd b/mobile/locales/en-US/chrome/browser.dtd index b3488b463e2a..e14b9a73e5a8 100644 --- a/mobile/locales/en-US/chrome/browser.dtd +++ b/mobile/locales/en-US/chrome/browser.dtd @@ -111,5 +111,6 @@ + diff --git a/mobile/locales/en-US/chrome/webapps.dtd b/mobile/locales/en-US/chrome/webapps.dtd new file mode 100644 index 000000000000..b1c548d59033 --- /dev/null +++ b/mobile/locales/en-US/chrome/webapps.dtd @@ -0,0 +1,6 @@ + + + + + + diff --git a/mobile/locales/jar.mn b/mobile/locales/jar.mn index 0961687a145b..58723593baf6 100644 --- a/mobile/locales/jar.mn +++ b/mobile/locales/jar.mn @@ -16,6 +16,7 @@ locale/@AB_CD@/browser/sync.dtd (%chrome/sync.dtd) locale/@AB_CD@/browser/sync.properties (%chrome/sync.properties) locale/@AB_CD@/browser/prompt.dtd (%chrome/prompt.dtd) + locale/@AB_CD@/browser/webapps.dtd (%chrome/webapps.dtd) locale/@AB_CD@/browser/feedback.dtd (%chrome/feedback.dtd) locale/@AB_CD@/browser/phishing.dtd (%chrome/phishing.dtd) locale/@AB_CD@/browser/bookmarks.json (bookmarks.json) diff --git a/mobile/themes/core/browser.css b/mobile/themes/core/browser.css index b3358f11121f..50c46a3ef5b2 100644 --- a/mobile/themes/core/browser.css +++ b/mobile/themes/core/browser.css @@ -1329,6 +1329,25 @@ setting { z-index: 500; } +/* openwebapps capabilities ------------------------------------------------------------ */ +.webapps-noperm description.webapps-perm-requested-hint { + display: block; +} + +.webapps-perm description.webapps-perm-requested-hint { + display: none; +} + +#webapps-icon { + width: 48px; + height: 48px; + margin: @margin_normal@; +} + +#webapps-title { + -moz-margin-end: @margin_normal@; +} + /* Android menu ------------------------------------------------------------ */ #appmenu { background: rgba(255,255,255,0.95); diff --git a/mobile/themes/core/gingerbread/browser.css b/mobile/themes/core/gingerbread/browser.css index 9b5750affbf9..844282121217 100644 --- a/mobile/themes/core/gingerbread/browser.css +++ b/mobile/themes/core/gingerbread/browser.css @@ -1311,6 +1311,25 @@ setting { z-index: 500; } +/* openwebapps capabilities ------------------------------------------------------------ */ +.webapps-noperm description.webapps-perm-requested-hint { + display: block; +} + +.webapps-perm description.webapps-perm-requested-hint { + display: none; +} + +#webapps-icon { + width: 32px; + height: 32px; + margin: @margin_normal@; +} + +#webapps-title { + -moz-margin-end: @margin_normal@; +} + /* Android menu ------------------------------------------------------------ */ #appmenu { background: @color_background_default@; diff --git a/mobile/themes/core/gingerbread/platform.css b/mobile/themes/core/gingerbread/platform.css index b87f33087b92..d266e8a63d87 100644 --- a/mobile/themes/core/gingerbread/platform.css +++ b/mobile/themes/core/gingerbread/platform.css @@ -287,12 +287,13 @@ toolbarbutton[open="true"] { list-style-image: url("chrome://browser/skin/images/check-selected-hdpi.png"); } +.button-checkbox > .button-box, .button-checkbox:hover:active > .button-box, .button-checkbox[checked="true"] > .button-box { padding-top: @padding_tiny@; padding-bottom: @padding_xsmall@; - -moz-padding-start: @margin_small@; - -moz-padding-end: @margin_small@; + -moz-padding-start: @padding_small@; + -moz-padding-end: @padding_small@; } /* radio buttons ----------------------------------------------------------- */