Bug 565718 - Adds module for a zoom indicator in the browser's URL bar. r?dao draft
authorKatie Broida[:ktbee] <kbroida@gmail.com>
Thu, 11 Aug 2016 14:14:36 -0400
changeset 400015 c18a914f2f6d89e26d5b4a83bdc837a657d5b09f
parent 399698 233ab21b64b5d5e9f2f16ea2d4cfb4c8b293c5c4
child 528135 ad9056b02200940c62849982b8b98e51e222d566
push id26071
push userbmo:kbroida@gmail.com
push dateFri, 12 Aug 2016 15:45:09 +0000
reviewersdao
bugs565718
milestone51.0a1
Bug 565718 - Adds module for a zoom indicator in the browser's URL bar. r?dao This indicator doesn't show if there is already zoom controls added from Customize Mode. MozReview-Commit-ID: F9CJIvPUiuh
browser/base/content/browser-fullZoom.js
browser/base/content/browser.xul
browser/components/nsBrowserGlue.js
browser/locales/en-US/chrome/browser/browser.dtd
browser/locales/en-US/chrome/browser/browser.properties
browser/modules/URLBarZoom.jsm
browser/modules/moz.build
browser/modules/test/browser.ini
browser/modules/test/browser_urlBar_zoom.js
browser/themes/linux/browser.css
browser/themes/osx/browser.css
browser/themes/windows/browser.css
--- a/browser/base/content/browser-fullZoom.js
+++ b/browser/base/content/browser-fullZoom.js
@@ -189,56 +189,56 @@ var FullZoom = {
     }
 
     // Ignore all pending async zoom accesses in the browser.  Pending accesses
     // that started before the location change will be prevented from applying
     // to the new location.
     this._ignorePendingZoomAccesses(browser);
 
     if (!aURI || (aIsTabSwitch && !this.siteSpecific)) {
-      this._notifyOnLocationChange();
+      this._notifyOnLocationChange(browser);
       return;
     }
 
     // Avoid the cps roundtrip and apply the default/global pref.
     if (aURI.spec == "about:blank") {
       this._applyPrefToZoom(undefined, browser,
-                            this._notifyOnLocationChange.bind(this));
+                            this._notifyOnLocationChange.bind(this, browser));
       return;
     }
 
     // Media documents should always start at 1, and are not affected by prefs.
     if (!aIsTabSwitch && browser.isSyntheticDocument) {
       ZoomManager.setZoomForBrowser(browser, 1);
       // _ignorePendingZoomAccesses already called above, so no need here.
-      this._notifyOnLocationChange();
+      this._notifyOnLocationChange(browser);
       return;
     }
 
     // See if the zoom pref is cached.
     let ctxt = this._loadContextFromBrowser(browser);
     let pref = this._cps2.getCachedByDomainAndName(aURI.spec, this.name, ctxt);
     if (pref) {
       this._applyPrefToZoom(pref.value, browser,
-                            this._notifyOnLocationChange.bind(this));
+                            this._notifyOnLocationChange.bind(this, browser));
       return;
     }
 
     // It's not cached, so we have to asynchronously fetch it.
     let value = undefined;
     let token = this._getBrowserToken(browser);
     this._cps2.getByDomainAndName(aURI.spec, this.name, ctxt, {
       handleResult: function (resultPref) { value = resultPref.value; },
       handleCompletion: function () {
         if (!token.isCurrent) {
-          this._notifyOnLocationChange();
+          this._notifyOnLocationChange(browser);
           return;
         }
         this._applyPrefToZoom(value, browser,
-                              this._notifyOnLocationChange.bind(this));
+                              this._notifyOnLocationChange.bind(this, browser));
       }.bind(this)
     });
   },
 
   // update state of zoom type menu item
 
   updateMenu: function FullZoom_updateMenu() {
     var menuItem = document.getElementById("toggle_zoom");
@@ -286,17 +286,17 @@ var FullZoom = {
    * @return A promise which resolves when the zoom reset has been applied.
    */
   reset: function FullZoom_reset(browser = gBrowser.selectedBrowser) {
     let token = this._getBrowserToken(browser);
     let result = this._getGlobalValue(browser).then(value => {
       if (token.isCurrent) {
         ZoomManager.setZoomForBrowser(browser, value === undefined ? 1 : value);
         this._ignorePendingZoomAccesses(browser);
-        Services.obs.notifyObservers(null, "browser-fullZoom:zoomReset", "");
+        Services.obs.notifyObservers(browser, "browser-fullZoom:zoomReset", "");
       }
     });
     this._removePref(browser);
     return result;
   },
 
   /**
    * Set the zoom level for a given browser.
@@ -354,17 +354,17 @@ var FullZoom = {
 
   /**
    * Saves the zoom level of the page in the given browser to the content
    * prefs store.
    *
    * @param browser  The zoom of this browser will be saved.  Required.
    */
   _applyZoomToPref: function FullZoom__applyZoomToPref(browser) {
-    Services.obs.notifyObservers(null, "browser-fullZoom:zoomChange", "");
+    Services.obs.notifyObservers(browser, "browser-fullZoom:zoomChange", "");
     if (!this.siteSpecific ||
         gInPrintPreviewMode ||
         browser.isSyntheticDocument)
       return;
 
     this._cps2.set(browser.currentURI.spec, this.name,
                    ZoomManager.getZoomForBrowser(browser),
                    this._loadContextFromBrowser(browser), {
@@ -375,17 +375,17 @@ var FullZoom = {
   },
 
   /**
    * Removes from the content prefs store the zoom level of the given browser.
    *
    * @param browser  The zoom of this browser will be removed.  Required.
    */
   _removePref: function FullZoom__removePref(browser) {
-    Services.obs.notifyObservers(null, "browser-fullZoom:zoomReset", "");
+    Services.obs.notifyObservers(browser, "browser-fullZoom:zoomReset", "");
     if (browser.isSyntheticDocument)
       return;
     let ctxt = this._loadContextFromBrowser(browser);
     this._cps2.removeByDomainAndName(browser.currentURI.spec, this.name, ctxt, {
       handleCompletion: function () {
         this._isNextContentPrefChangeInternal = true;
       }.bind(this),
     });
@@ -512,19 +512,19 @@ var FullZoom = {
   },
 
   /**
    * Asynchronously broadcasts "browser-fullZoom:location-change" so that
    * listeners can be notified when the zoom levels on those pages change.
    * The notification is always asynchronous so that observers are guaranteed a
    * consistent behavior.
    */
-  _notifyOnLocationChange: function FullZoom__notifyOnLocationChange() {
+  _notifyOnLocationChange: function FullZoom__notifyOnLocationChange(browser) {
     this._executeSoon(function () {
-      Services.obs.notifyObservers(null, "browser-fullZoom:location-change", "");
+      Services.obs.notifyObservers(browser, "browser-fullZoom:location-change", "");
     });
   },
 
   _executeSoon: function FullZoom__executeSoon(callback) {
     if (!callback)
       return;
     Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
   },
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -777,16 +777,20 @@
                        class="urlbar-icon"
                        hidden="true"
                        tooltiptext="&pageReportIcon.tooltip;"
                        onclick="gPopupBlockerObserver.onReportButtonClick(event);"/>
                 <image id="reader-mode-button"
                        class="urlbar-icon"
                        hidden="true"
                        onclick="ReaderParent.buttonClick(event);"/>
+                <toolbarbutton id="urlbar-zoom-button"
+                       onclick="FullZoom.reset();"
+                       tooltiptext="&urlbar.zoomReset.tooltip;"
+                       hidden="true"/>
               </hbox>
               <hbox id="userContext-icons" hidden="true">
                 <label id="userContext-label"/>
                 <image id="userContext-indicator"/>
               </hbox>
               <toolbarbutton id="urlbar-go-button"
                              class="chromeclass-toolbar-additional"
                              onclick="gURLBar.handleCommand(event);"
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -56,16 +56,17 @@ XPCOMUtils.defineLazyServiceGetter(this,
   ["SelfSupportBackend", "resource:///modules/SelfSupportBackend.jsm"],
   ["SessionStore", "resource:///modules/sessionstore/SessionStore.jsm"],
   ["ShellService", "resource:///modules/ShellService.jsm"],
   ["SimpleServiceDiscovery", "resource://gre/modules/SimpleServiceDiscovery.jsm"],
   ["TabCrashHandler", "resource:///modules/ContentCrashHandlers.jsm"],
   ["TabGroupsMigrator", "resource:///modules/TabGroupsMigrator.jsm"],
   ["Task", "resource://gre/modules/Task.jsm"],
   ["UITour", "resource:///modules/UITour.jsm"],
+  ["URLBarZoom", "resource:///modules/URLBarZoom.jsm"],
   ["WebChannel", "resource://gre/modules/WebChannel.jsm"],
   ["WindowsRegistry", "resource://gre/modules/WindowsRegistry.jsm"],
   ["webrtcUI", "resource:///modules/webrtcUI.jsm"],
 ].forEach(([name, resource]) => XPCOMUtils.defineLazyModuleGetter(this, name, resource));
 
 if (AppConstants.MOZ_CRASHREPORTER) {
   XPCOMUtils.defineLazyModuleGetter(this, "PluginCrashReporter",
                                     "resource:///modules/ContentCrashHandlers.jsm");
@@ -685,16 +686,17 @@ BrowserGlue.prototype = {
 
     ContentClick.init();
     RemotePrompt.init();
     Feeds.init();
     ContentPrefServiceParent.init();
 
     LoginManagerParent.init();
     ReaderParent.init();
+    URLBarZoom.init();
 
     SelfSupportBackend.init();
 
     // Ensure we keep track of places/pw-mananager undo by init'ing this early.
     Cu.import("resource:///modules/AutoMigrate.jsm");
 
     if (!AppConstants.RELEASE_BUILD) {
       let themeName = gBrowserBundle.GetStringFromName("deveditionTheme.name");
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -226,16 +226,18 @@ These should match what Safari and other
 <!ENTITY urlbar.pointerLockNotificationAnchor.label     "Change whether the site can hide the pointer">
 <!ENTITY urlbar.servicesNotificationAnchor.label        "View the service install message">
 <!ENTITY urlbar.translateNotificationAnchor.label       "Translate this page">
 <!ENTITY urlbar.translatedNotificationAnchor.label      "Manage page translation">
 <!ENTITY urlbar.emeNotificationAnchor.label             "Manage use of DRM software">
 
 <!ENTITY urlbar.openHistoryPopup.tooltip                "Show history">
 
+<!ENTITY urlbar.zoomReset.tooltip     "Reset zoom level">
+
 <!ENTITY searchItem.title             "Search">
 
 <!-- Toolbar items --> 
 <!ENTITY homeButton.label             "Home">
 
 <!ENTITY bookmarksButton.label          "Bookmarks">
 <!ENTITY bookmarksCmd.commandkey "b">
 
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -265,16 +265,20 @@ menuUndoCloseWindowSingleTabLabel=#1
 
 # Unified Back-/Forward Popup
 tabHistory.current=Stay on this page
 tabHistory.goBack=Go back to this page
 tabHistory.goForward=Go forward to this page
 
 # URL Bar
 pasteAndGo.label=Paste & Go
+# LOCALIZATION NOTE(urlbar-zoom-button.label): %S is the current zoom level,
+# %% will be displayed as a single % character (% is commonly used to define
+# format specifiers, so it needs to be escaped).
+urlbar-zoom-button.label = %S%%
 
 # Block autorefresh
 refreshBlocked.goButton=Allow
 refreshBlocked.goButton.accesskey=A
 refreshBlocked.refreshLabel=%S prevented this page from automatically reloading.
 refreshBlocked.redirectLabel=%S prevented this page from automatically redirecting to another page.
 
 # General bookmarks button
new file mode 100644
--- /dev/null
+++ b/browser/modules/URLBarZoom.jsm
@@ -0,0 +1,51 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* 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";
+
+this.EXPORTED_SYMBOLS = [ "URLBarZoom" ];
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+var URLBarZoom = {
+
+  init: function(aWindow) {
+    // Register ourselves with the service so we know when the zoom prefs change.
+    Services.obs.addObserver(updateZoomButton, "browser-fullZoom:zoomChange", false);
+    Services.obs.addObserver(updateZoomButton, "browser-fullZoom:zoomReset", false);
+    Services.obs.addObserver(updateZoomButton, "browser-fullZoom:location-change", false);
+  },
+}
+
+function updateZoomButton(aSubject, aTopic) {
+  let win = aSubject.ownerDocument.defaultView;
+  let customizableZoomControls = win.document.getElementById("zoom-controls");
+  let zoomResetButton = win.document.getElementById("urlbar-zoom-button");
+  let zoomFactor = Math.round(win.ZoomManager.zoom * 100);
+
+  // Ensure that zoom controls haven't already been added to browser in Customize Mode
+  if (customizableZoomControls &&
+      customizableZoomControls.getAttribute("cui-areatype") == "toolbar") {
+    zoomResetButton.hidden = true;
+    return;
+  }
+  if (zoomFactor != 100) {
+    // Check if zoom button is visible and update label if it is
+    if (zoomResetButton.hidden) {
+      zoomResetButton.hidden = false;
+    }
+    // Only allow pulse animation for zoom changes, not tab switching
+    if (aTopic != "browser-fullZoom:location-change") {
+      zoomResetButton.setAttribute("animate", "true");
+    } else {
+      zoomResetButton.removeAttribute("animate");
+    }
+    zoomResetButton.setAttribute("label",
+        win.gNavigatorBundle.getFormattedString("urlbar-zoom-button.label", [zoomFactor]));
+  // Hide button if zoom is at 100%
+  } else {
+      zoomResetButton.hidden = true;
+  }
+}
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -40,16 +40,17 @@ EXTRA_JS_MODULES += [
     'RecentWindow.jsm',
     'RemotePrompt.jsm',
     'Sanitizer.jsm',
     'SelfSupportBackend.jsm',
     'SitePermissions.jsm',
     'Social.jsm',
     'TabGroupsMigrator.jsm',
     'TransientPrefs.jsm',
+    'URLBarZoom.jsm',
     'webrtcUI.jsm',
 ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':
     EXTRA_JS_MODULES += [
         'Windows8WindowFrameColor.jsm',
         'WindowsJumpLists.jsm',
         'WindowsPreviewPerTab.jsm',
--- a/browser/modules/test/browser.ini
+++ b/browser/modules/test/browser.ini
@@ -6,16 +6,17 @@ support-files =
 [browser_BrowserUITelemetry_defaults.js]
 [browser_BrowserUITelemetry_sidebar.js]
 [browser_BrowserUITelemetry_syncedtabs.js]
 [browser_CaptivePortalWatcher.js]
 skip-if = os == "linux" || os == "win" || (os == "mac" && debug && e10s) # Bug 1279491, Bug 1287714 for OS X debug e10s
 [browser_ProcessHangNotifications.js]
 skip-if = !e10s
 [browser_ContentSearch.js]
+[browser_urlBar_zoom.js]
 support-files =
   contentSearch.js
   contentSearchBadImage.xml
   contentSearchSuggestions.sjs
   contentSearchSuggestions.xml
 [browser_NetworkPrioritizer.js]
 [browser_SelfSupportBackend.js]
 support-files =
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser_urlBar_zoom.js
@@ -0,0 +1,73 @@
+/* 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";
+
+var initialPageZoom = ZoomManager.zoom;
+const kTimeoutInMS = 20000;
+
+add_task(function* () {
+  info("Confirm whether the browser zoom is set to the default level");
+  is(initialPageZoom, 1, "Page zoom is set to default (100%)");
+  let zoomResetButton = document.getElementById("urlbar-zoom-button");
+  is(zoomResetButton.hidden, true, "Zoom reset button is currently hidden");
+
+  info("Change zoom and confirm zoom button appears");
+  let labelUpdatePromise = BrowserTestUtils.waitForAttribute("label", zoomResetButton);
+  FullZoom.enlarge();
+  yield labelUpdatePromise;
+  info("Zoom increased to " + Math.floor(ZoomManager.zoom * 100) + "%");
+  is(zoomResetButton.hidden, false, "Zoom reset button is now visible");
+  let pageZoomLevel = Math.floor(ZoomManager.zoom * 100);
+  let expectedZoomLevel = 110;
+  let buttonZoomLevel = parseInt(zoomResetButton.getAttribute("label"), 10);
+  is(buttonZoomLevel, expectedZoomLevel, ("Button label updated successfully to " + Math.floor(ZoomManager.zoom * 100) + "%"));
+
+  let zoomResetPromise = promiseObserverNotification("browser-fullZoom:zoomReset");
+  zoomResetButton.click();
+  yield zoomResetPromise;
+  pageZoomLevel = Math.floor(ZoomManager.zoom * 100);
+  expectedZoomLevel = 100;
+  is(pageZoomLevel, expectedZoomLevel, "Clicking zoom button successfully resets browser zoom to 100%");
+  is(zoomResetButton.hidden, true, "Zoom reset button returns to being hidden");
+
+});
+
+add_task(function* () {
+  info("Confirm that URL bar zoom button doesn't appear when customizable zoom widget is added to toolbar");
+  CustomizableUI.addWidgetToArea("zoom-controls", CustomizableUI.AREA_NAVBAR);
+  let zoomCustomizableWidget = document.getElementById("zoom-reset-button");
+  let zoomResetButton = document.getElementById("urlbar-zoom-button");
+  let zoomChangePromise = promiseObserverNotification("browser-fullZoom:zoomChange");
+  FullZoom.enlarge();
+  yield zoomChangePromise;
+  is(zoomResetButton.hidden, true, "URL zoom button remains hidden despite zoom increase");
+  is(parseInt(zoomCustomizableWidget.label, 10), 110, "Customizable zoom widget's label has updated to " + zoomCustomizableWidget.label);
+});
+
+add_task(function* asyncCleanup() {
+  // reset zoom level and customizable widget
+  ZoomManager.zoom = initialPageZoom;
+  is(ZoomManager.zoom, 1, "Zoom level was restored");
+  if (document.getElementById("zoom-controls")) {
+    CustomizableUI.removeWidgetFromArea("zoom-controls", CustomizableUI.AREA_NAVBAR);
+    ok(!document.getElementById("zoom-controls"),"Customizable zoom widget removed from toolbar");
+  }
+
+});
+
+function promiseObserverNotification(aObserver) {
+  let deferred = Promise.defer();
+  function notificationCallback(e) {
+    Services.obs.removeObserver(notificationCallback, aObserver, false);
+    clearTimeout(timeoutId);
+    deferred.resolve();
+  }
+  let timeoutId = setTimeout(() => {
+    Services.obs.removeObserver(notificationCallback, aObserver, false);
+    deferred.reject("Notification '" + aObserver + "' did not happen within 20 seconds.");
+  }, kTimeoutInMS);
+  Services.obs.addObserver(notificationCallback, aObserver, false);
+  return deferred.promise;
+}
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -942,16 +942,53 @@ menuitem:not([type]):not(.menuitem-toolt
 
 .urlbar-icon {
   padding: 0 3px;
   /* 16x16 icon with border-box sizing */
   width: 22px;
   height: 16px;
 }
 
+/* ::::: URL Bar Zoom Reset Button ::::: */
+@keyframes urlbar-zoom-reset-pulse {
+  0% {
+  transform: scale(0);
+  }
+  100% {
+  transform: scale(1.5);
+  }
+}
+
+#urlbar-zoom-button {
+  -moz-appearance: none;
+  margin: 0 3px;
+  font-size: .8em;
+  padding: 0 8px;
+  border-radius: 1em;
+  background-color: hsla(0,0%,0%,.05);
+  border: 1px solid ThreeDLightShadow;
+}
+
+#urlbar-zoom-button[animate="true"] {
+  animation-name: urlbar-zoom-reset-pulse;
+  animation-duration: 250ms;
+}
+
+#urlbar-zoom-button:hover:active {
+  background-color: hsla(0,0%,0%,.1);
+}
+
+#urlbar-zoom-button > .toolbarbutton-text {
+  display: -moz-box;
+}
+
+#urlbar-zoom-button > .toolbarbutton-icon {
+  display: none;
+}
+
 #urlbar-search-footer {
   border-top: 1px solid hsla(210,4%,10%,.14);
   background-color: hsla(210,4%,10%,.07);
 }
 
 #urlbar-search-settings {
   -moz-appearance: none;
   -moz-user-focus: ignore;
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -1650,16 +1650,51 @@ toolbar .toolbarbutton-1 > .toolbarbutto
 
 .urlbar-icon {
   padding: 0 3px;
   /* 16x16 icon with border-box sizing */
   width: 22px;
   height: 16px;
 }
 
+/* ::::: URL Bar Zoom Reset Button ::::: */
+@keyframes urlbar-zoom-reset-pulse {
+  0% {
+    transform: scale(0);
+  }
+  100% {
+    transform: scale(1.5);
+  }
+}
+
+#urlbar-zoom-button {
+  margin: 0 3px;
+  font-size: .8em;
+  padding: 0 8px;
+  border-radius: 1em;
+  background-color: hsla(0,0%,0%,.05);
+  border: 1px solid hsla(0,0%,0%,.1);
+}
+
+#urlbar-zoom-button[animate="true"] {
+  animation-name: urlbar-zoom-reset-pulse;
+  animation-duration: 250ms;
+}
+#urlbar-zoom-button:hover:active {
+  background-color: hsla(0,0%,0%,.1);
+}
+
+#urlbar-zoom-button > .toolbarbutton-text {
+  display: -moz-box;
+}
+
+#urlbar-zoom-button > .toolbarbutton-icon {
+  display: none;
+}
+
 #urlbar-search-footer {
   border-top: 1px solid hsla(210,4%,10%,.14);
   background-color: hsla(210,4%,10%,.07);
 }
 
 #urlbar-search-settings {
   -moz-appearance: none;
   -moz-user-focus: ignore;
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -1362,16 +1362,53 @@ html|*.urlbar-input:-moz-lwtheme::-moz-p
 
 .urlbar-icon {
   padding: 0 3px;
   /* 16x16 icon with border-box sizing */
   width: 22px;
   height: 16px;
 }
 
+/* ::::: URL Bar Zoom Reset Button ::::: */
+@keyframes urlbar-zoom-reset-pulse {
+  0% {
+  transform: scale(0);
+  }
+  100% {
+  transform: scale(1.5);
+  }
+}
+
+#urlbar-zoom-button {
+  -moz-appearance: none;
+  margin: 0 3px;
+  font-size: .8em;
+  padding: 0 8px;
+  border-radius: 1em;
+  background-color: hsla(0,0%,0%,.05);
+  border: 1px solid ThreeDLightShadow;
+}
+
+#urlbar-zoom-button[animate="true"] {
+  animation-name: urlbar-zoom-reset-pulse;
+  animation-duration: 250ms;
+}
+
+#urlbar-zoom-button:hover:active {
+  background-color: hsla(0,0%,0%,.1);
+}
+
+#urlbar-zoom-button > .toolbarbutton-text {
+  display: -moz-box;
+}
+
+#urlbar-zoom-button > .toolbarbutton-icon {
+  display: none;
+}
+
 .search-go-container {
   padding: 2px 2px;
 }
 
 #urlbar-search-footer {
   border-top: 1px solid hsla(210,4%,10%,.14);
   background-color: hsla(210,4%,10%,.07);
 }