Bug 530835 - Flash: Chrome UI shows up on background of flash videos [r=gavin.sharp]
authorBenjamin Stover <bstover@mozilla.com>
Fri, 04 Dec 2009 12:03:56 -0800
changeset 65889 dd4111f9e4c281803a512a41d39204af42fdefea
parent 65888 421d918cea54fb1ecbe2cdeee8e4ef36549864ed
child 65890 338ee715b0f4b37cfc46081fb8c8413bbd6a94c2
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgavin
bugs530835
Bug 530835 - Flash: Chrome UI shows up on background of flash videos [r=gavin.sharp]
mobile/chrome/content/Util.js
mobile/chrome/content/browser-ui.js
mobile/chrome/content/browser.js
mobile/chrome/tests/browser_rect.js
--- a/mobile/chrome/content/Util.js
+++ b/mobile/chrome/content/Util.js
@@ -171,21 +171,25 @@ let Util = {
   // process.
   forceOnline: function forceOnline() {
 #ifdef MOZ_PLATFORM_HILDON
     gIOService.offline = false;
 #endif
   }
 };
 
+/**
+ * Cache of commonly used elements.
+ */
 let Elements = {};
 
 [
   ["browserBundle",      "bundle_browser"],
   ["contentShowing",     "bcast_contentShowing"],
+  ["stack",              "stack"],
 ].forEach(function (elementGlobal) {
   let [name, id] = elementGlobal;
   Elements.__defineGetter__(name, function () {
     let element = document.getElementById(id);
     if (!element)
       return null;
     delete Elements[name];
     return Elements[name] = element;
@@ -309,17 +313,17 @@ Rect.prototype = {
     this.left = x;
     this.top = y;
     this.right = x+w;
     this.bottom = y+h;
 
     return this;
   },
 
-  setBounds: function(t, l, b, r) {
+  setBounds: function(l, t, r, b) {
     this.top = t;
     this.left = l;
     this.bottom = b;
     this.right = r;
 
     return this;
   },
 
@@ -447,13 +451,40 @@ Rect.prototype = {
     this.bottom = f.call(this, this.bottom);
     return this;
   },
 
   /** Ensure this rectangle is inside the other, if possible. Preserves w, h. */
   translateInside: function translateInside(other) {
     let offsetX = (this.left < other.left ? other.left - this.left :
         (this.right > other.right ? this.right - other.right : 0));
-   let offsetY = (this.top < other.top ? other.top - this.top :
+    let offsetY = (this.top < other.top ? other.top - this.top :
         (this.bottom > other.bottom ? this.bottom - other.bottom : 0));
     return this.translate(offsetX, offsetY);
-  }
+  },
+
+  /** Subtract other area from this. Returns array of rects whose union is this-other. */
+  subtract: function subtract(other) {
+    let r = new Rect(0, 0, 0, 0);
+    let result = [];
+    other = other.intersect(this);
+    if (other.isEmpty())
+      return [this.clone()];
+
+    // left strip
+    r.setBounds(this.left, this.top, other.left, this.bottom);
+    if (!r.isEmpty())
+      result.push(r.clone());
+    // inside strip
+    r.setBounds(other.left, this.top, other.right, other.top);
+    if (!r.isEmpty())
+      result.push(r.clone());
+    r.setBounds(other.left, other.bottom, other.right, this.bottom);
+    if (!r.isEmpty())
+      result.push(r.clone());
+    // right strip
+    r.setBounds(other.right, this.top, this.right, this.bottom);
+    if (!r.isEmpty())
+      result.push(r.clone());
+
+    return result;
+  },
 };
--- a/mobile/chrome/content/browser-ui.js
+++ b/mobile/chrome/content/browser-ui.js
@@ -278,53 +278,64 @@ var BrowserUI = {
       this.lockToolbar();
       this._dialogs.push(aDialog);
       document.getElementById("toolbar-main").setAttribute("dialog", "true");
       Elements.contentShowing.setAttribute("disabled", "true");
     }
   },
 
   popDialog : function popDialog() {
-    // Passing null means we pop the topmost dialog
     if (this._dialogs.length) {
       this._dialogs.pop();
       this.unlockToolbar();
     }
 
     // If no more dialogs are being displayed, remove the attr for CSS
     if (!this._dialogs.length) {
       document.getElementById("toolbar-main").removeAttribute("dialog");
       Elements.contentShowing.removeAttribute("disabled");
     }
   },
 
   pushPopup: function pushPopup(aPanel, aElements) {
-    this._updatePopup();
+    this._hidePopup();
     this._popup =  { "panel": aPanel, 
                      "elements": (aElements instanceof Array) ? aElements : [aElements] };
+    this._dispatchPopupChanged();
   },
 
   popPopup: function popPopup() {
     this._popup = null;
+    this._dispatchPopupChanged();
   },
 
-  _updatePopup: function _updateContextualPanel(aEvent) {
+  _dispatchPopupChanged: function _dispatchPopupChanged() {
+    let stack = document.getElementById("stack");
+    let event = document.createEvent("Events");
+    event.initEvent("PopupChanged", true, false);
+    event.popup = this._popup;
+    stack.dispatchEvent(event);
+  },
+
+  _hidePopup: function _hidePopup() {
     if (!this._popup)
       return;
-
-    let element = this._popup.elements;
+    let panel = this._popup.panel;
+    if (panel.hide)
+      panel.hide();
+  },
+  
+  _isEventInsidePopup: function _isEventInsidePopup(aEvent) {
+    if (!this._popup)
+      return;
+    let elements = this._popup.elements;
     let targetNode = aEvent ? aEvent.target : null;
-    while (targetNode && element.indexOf(targetNode) == -1)
+    while (targetNode && elements.indexOf(targetNode) == -1)
       targetNode = targetNode.parentNode;
-
-    if (targetNode == null) {
-      let panel = this._popup.panel;
-      if (panel.hide)
-        panel.hide();
-    }
+    return targetNode ? true : false;
   },
 
   switchPane : function(id) {
     let button = document.getElementsByAttribute("linkedpanel", id)[0];
     if (button)
       button.checked = true;
     document.getElementById("panel-items").selectedPanel = document.getElementById(id);
   },
@@ -675,17 +686,18 @@ var BrowserUI = {
       case "keypress":
         if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
           let dialog = this.activeDialog;
           if (dialog)
             dialog.close();
         }
         break;
       case "mousedown":
-        this._updatePopup(aEvent);
+        if (!this._isEventInsidePopup(aEvent))
+          this._hidePopup();
 
         if (aEvent.detail == 2 &&
             aEvent.button == 0 &&
             gPrefService.getBoolPref("browser.urlbar.doubleClickSelectsAll")) {
           this._edit.editor.selectAll();
           aEvent.preventDefault();
         }
         break;
@@ -947,17 +959,18 @@ var BookmarkHelper = {
     let top = toolbar.top + toolbar.boxObject.height;
 
     this._panel = document.getElementById("bookmark-container");
     this._panel.top = (top < 0 ? 0 : top);
     this._panel.hidden = false;
     BrowserUI.pushPopup(this, this._panel);
 
     let self = this;
-    Util.executeSoon(function() { self._editor.startEditing(); });
+    Browser.forceChromeReflow();
+    self._editor.startEditing();
   },
 
   save: function BH_save() {
     this._editor.stopEditing(true);
   },
   
   hide: function BH_hide() {
     BrowserUI.updateStar();
--- a/mobile/chrome/content/browser.js
+++ b/mobile/chrome/content/browser.js
@@ -2776,16 +2776,17 @@ Tab.prototype = {
   },
 
   _destroyBrowser: function() {
     if (this._browser) {
       document.getElementById("browsers").removeChild(this._browser);
       this._browser = null;
 
       if (this._loading) {
+        this._loading = false;
         Browser._browserView.commitBatchOperation();
         clearTimeout(this._loadingTimeout);
         delete this._loadingTimeout;
       }
     }
   },
 
   /** Serializes as much state as possible of the current content.  */
@@ -2956,30 +2957,35 @@ const nsIObjectLoadingContent = Ci.nsIOb
  * Allows fast-path embed rendering by letting objects know where to absolutely
  * render on the screen.
  */
 function PluginObserver(bv) {
   this._emptyRect = new Rect(0, 0, 0, 0);
   this._contentShowing = document.getElementById("observe_contentShowing");
   this._bv = bv;
   this._started = false;
+  this._isRendering = false;
 }
 
 PluginObserver.prototype = {
+  // When calculating critical rect, subtract N pixels around popup boxes
+  POPUP_PADDING: 4,
+
   /** Starts flash objects fast path. */
   start: function() {
     if (this._started)
       return;
     this._started = true;
 
     document.getElementById("tabs-container").addEventListener("TabSelect", this, false);
     this._contentShowing.addEventListener("broadcast", this, false);
     let browsers = document.getElementById("browsers");
     browsers.addEventListener("RenderStateChanged", this, false);
     gObserverService.addObserver(this, "plugin-changed-event", false);
+    Elements.stack.addEventListener("PopupChanged", this, false);
 
     let browser = Browser.selectedBrowser;
     if (browser) {
       browser.addEventListener("ZoomChanged", this, false);
       browser.addEventListener("MozAfterPaint", this, false);
     }
   },
 
@@ -2989,16 +2995,17 @@ PluginObserver.prototype = {
       return;
     this._started = false;
 
     document.getElementById("tabs-container").removeEventListener("TabSelect", this, false);
     this._contentShowing.removeEventListener("broadcast", this, false);
     let browsers = document.getElementById("browsers");
     browsers.removeEventListener("RenderStateChanged", this, false);
     gObserverService.removeObserver(this, "plugin-changed-event");
+    Elements.stack.removeEventListener("PopupChanged", this, false);
 
     let browser = Browser.selectedBrowser;
     if (browser) {
       browser.removeEventListener("ZoomChanged", this, false);
       browser.removeEventListener("MozAfterPaint", this, false);
     }
   },
 
@@ -3042,23 +3049,26 @@ PluginObserver.prototype = {
 
   /** Update the current browser's flash objects. */
   updateCurrentBrowser: function updateCurrentBrowser() {
     let doc = Browser.selectedTab.browser.contentDocument;
     if (!doc.pluginCache)
       return;
 
     let rect = this.getCriticalRect();
-    if (rect == this._emptyRect) {
-      // Always stop rendering immediately.
+    if (this._isRendering) {
+      // Update immediately if not just starting to render
+      if (rect == this._emptyRect)
+        this._isRendering = false;
       this.updateEmbedRegions(doc.pluginCache, rect);
     } else {
       // Wait a moment so that any chrome redraws occur first.
       let self = this;
       setTimeout(function() {
+        self._isRendering = true;
         // Recalculate critical rect so we don't render when we ought not to.
         self.updateEmbedRegions(doc.pluginCache, self.getCriticalRect());
       }, 0);
     }
   },
 
   /** More accurate version of finding the current visible region. Returns client coords. */
   getCriticalRect: function getCriticalRect() {
@@ -3074,18 +3084,42 @@ PluginObserver.prototype = {
     crit = Browser.browserViewToClientRect(crit);
 
     if (BrowserUI.isToolbarLocked()) {
       let urlbar = document.getElementById("toolbar-container");
       let urlbarRect = urlbar.getBoundingClientRect();
       // Subtract urlbar area from critical rect area.  In general subtracting a rect from another
       // results in a region that can be described by a union of rects.  Luckily in this case,
       // we can cheat because the resulting area is still just one rect.
-      crit.top = Math.max(urlbarRect.height, crit.top);
+      crit.top = Math.max(Math.round(urlbarRect.height) + 1, crit.top);
+      // XXX we add 1 to the height so that flash overlays don't leak into URL bar (otherwise
+      // you may see a strip of junk if you pop up the identity panel over flash)
     }
+
+    let popup = BrowserUI._popup;
+    if (popup) {
+      let p = this.POPUP_PADDING;
+      let elements = BrowserUI._popup.elements;
+      for (let i = elements.length - 1; i >= 0; i--) {
+        let popupRect = Rect.fromRect(elements[i].getBoundingClientRect()).expandToIntegers();
+        // XXX Our CSS shadows don't seem to be included in getBoundingClientRect.  Compensate
+        // with some padding; may need to be changed depending on the theme. Otherwise, flash
+        // can "leak into" the popup.
+        popupRect.setBounds(popupRect.left - p, popupRect.top - p, popupRect.right + p, popupRect.bottom + p);
+        let areaRects = crit.subtract(popupRect);
+        if (areaRects.length == 1) {
+          // Yay, critical region is still just a rect!
+          crit = areaRects[0];
+        } else if (areaRects.length > 1) {
+          // Critical region is a union of rects. Give up.
+          return this._emptyRect;
+        }
+      }
+    }
+
     return crit;
   },
 
   /**
    * Tell embedded objects where to absolutely render, using crit for clipping.
    * @param crit Specified in client coordinates
    */
   updateEmbedRegions: function updateEmbedRegions(objects, crit) {
--- a/mobile/chrome/tests/browser_rect.js
+++ b/mobile/chrome/tests/browser_rect.js
@@ -64,10 +64,39 @@ let tests = {
     let r2 = new Rect(0, 0, 0, 0);
     r1.expandToContain(r2);
     ok(r1.equals(new Rect(10, 10, 100, 100)), "expandToContain of rect and empty is rect");
 
     let r1 = new Rect(10, 10, 0, 0);
     let r2 = new Rect(0, 0, 0, 0);
     r1.expandToContain(r2);
     ok(r1.isEmpty(), "expandToContain of empty and empty is empty");
-  }
+  },
+
+  testSubtract: function testSubtract() {
+    function equals(rects1, rects2) {
+      return rects1.length == rects2.length && rects1.every(function(r, i) {
+        return r.equals(rects2[i]);
+      });
+    }
+
+    let r1 = new Rect(0, 0, 100, 100);
+    let r2 = new Rect(500, 500, 100, 100);
+    ok(equals(r1.subtract(r2), [r1]), "subtract area outside of region yields same region");
+
+    let r1 = new Rect(0, 0, 100, 100);
+    let r2 = new Rect(-10, -10, 50, 120);
+    ok(equals(r1.subtract(r2), [new Rect(40, 0, 60, 100)]), "subtracting vertical bar from edge leaves one rect");
+
+    let r1 = new Rect(0, 0, 100, 100);
+    let r2 = new Rect(-10, -10, 120, 50);
+    ok(equals(r1.subtract(r2), [new Rect(0, 40, 100, 60)]), "subtracting horizontal bar from edge leaves one rect");
+
+    let r1 = new Rect(0, 0, 100, 100);
+    let r2 = new Rect(40, 40, 20, 20);
+    ok(equals(r1.subtract(r2), [
+      new Rect(0, 0, 40, 100),
+      new Rect(40, 0, 20, 40),
+      new Rect(40, 60, 20, 40),
+      new Rect(60, 0, 40, 100)]),
+      "subtracting rect in middle leaves union of rects");
+  },
 };