poc to support moving a dialog after a popup upon activation

The code in Iframe.js was a test to fix the losing state problem
diff --git a/org.eclipse.scout.rt.ui.html.test/src/test/js/scout/desktop/DesktopSpec.js b/org.eclipse.scout.rt.ui.html.test/src/test/js/scout/desktop/DesktopSpec.js
index 6ccfa26..262c3b8 100644
--- a/org.eclipse.scout.rt.ui.html.test/src/test/js/scout/desktop/DesktopSpec.js
+++ b/org.eclipse.scout.rt.ui.html.test/src/test/js/scout/desktop/DesktopSpec.js
@@ -56,7 +56,7 @@
       expect(ntfc.fadeIn).toHaveBeenCalled();
       expect(desktop.notifications.indexOf(ntfc)).toBe(0);
       expect(desktop.$container.find('.desktop-notifications').length).toBe(1);
-      expect(desktop.$notification).not.toBe(null);
+      expect(desktop.$notifications).not.toBe(null);
     });
 
     it('schedules addNotification when desktop is not rendered', function() {
@@ -598,10 +598,6 @@
       return desktop.$container.children('.overlay-separator').nextAll().toArray();
     };
 
-    var formElt = function(form) {
-      return form.$container[0];
-    };
-
     var widgetHtmlElements = function(forms) {
       var formElts = [];
       forms.forEach(function(form) {
@@ -1159,6 +1155,36 @@
       expect(tabBox.currentView).toEqual(viewForm0);
       expect(desktopOverlayHtmlElements()).toEqual(widgetHtmlElements([dialog0]));
     });
+
+    it('brings dialog on top of a top level popup upon activation', function() {
+      expect(desktop.activeForm).toBe(null);
+
+      var dialog0 = formHelper.createFormWithOneField({
+        modal: false
+      });
+      desktop.showForm(dialog0);
+
+      var popup = scout.create('WidgetPopup', {
+        closeOnMouseDownOutside: false,
+        closeOnOtherPopupOpen: false,
+        widget: {
+          objectType: 'Label'
+        },
+        parent: desktop
+      });
+      popup.open();
+
+      // expect dialogs to be in the same order as opened
+      expect(desktopOverlayHtmlElements()).toEqual(widgetHtmlElements([dialog0, popup]));
+      expect(desktop.activeForm).toBe(dialog0);
+
+      desktop.activateForm(dialog0);
+      // expect dialog0 to be on top (= last in the DOM)
+      expect(desktopOverlayHtmlElements()).toEqual(widgetHtmlElements([popup, dialog0]));
+      expect(desktop.activeForm).toBe(dialog0);
+
+      popup.close();
+    });
   });
 
   describe('activeForm', function() {
@@ -1250,7 +1276,6 @@
       var outline = outlineHelper.createOutlineWithOneDetailForm();
       desktop.setOutline(outline);
       outline.selectNodes(outline.nodes[0]);
-      var detailForm = outline.nodes[0].detailForm;
       var dialog = formHelper.createFormWithOneField({
         displayHint: 'dialog'
       });
@@ -1881,7 +1906,7 @@
 
   describe('modal form', function() {
 
-    var view1, view2, view3;
+    var view1;
 
     beforeEach(function() {
       session._renderDesktop();
diff --git a/org.eclipse.scout.rt.ui.html/src/main/js/scout/desktop/Desktop.js b/org.eclipse.scout.rt.ui.html/src/main/js/scout/desktop/Desktop.js
index e46f6be..910c388 100644
--- a/org.eclipse.scout.rt.ui.html/src/main/js/scout/desktop/Desktop.js
+++ b/org.eclipse.scout.rt.ui.html/src/main/js/scout/desktop/Desktop.js
@@ -52,6 +52,7 @@
   this.openUriHandler = null;
   this.theme = null;
   this.dense = false;
+  this._glassPaneTargetFilters = [];
 
   this._addWidgetProperties(['viewButtons', 'menus', 'views', 'selectedViewTabs', 'dialogs', 'outline', 'messageBoxes', 'notifications', 'fileChoosers', 'addOns', 'keyStrokes', 'activeForm']);
 
@@ -109,6 +110,12 @@
   this.openUriHandler = scout.create('OpenUriHandler', {
     session: this.session
   });
+  this._glassPaneTargetFilters.push(function(targetElem, element) {
+    // Exclude all child elements of the given widget
+    // Use case: element is a popup and has tooltip open. The tooltip is displayed in the desktop and considered as glass pane target by the selector above
+    var target = scout.widget(targetElem);
+    return !element.has(target);
+  });
 };
 
 /**
@@ -831,12 +838,11 @@
     if (element.$container) {
       $glassPaneTargets = $glassPaneTargets.not(element.$container);
     }
-    // Exclude all child elements of the given widget
-    // Use case: element is a popup and has tooltip open. The tooltip is displayed in the desktop and considered as glass pane target by the selector above
     $glassPaneTargets = $glassPaneTargets.filter(function(i, targetElem) {
-      var target = scout.widget(targetElem);
-      return !element.has(target);
-    });
+      return this._glassPaneTargetFilters.every(function(filter) {
+        return filter(targetElem, element);
+      }, this);
+    }.bind(this));
   }
 
   var glassPaneTargets;
@@ -862,6 +868,20 @@
 };
 
 /**
+ * Adds a filter which is applied when the glass pane targets are collected.
+ * If the filter returns false, the target won't be accepted and not covered by a glass pane.
+ * @param filter a function with the parameter target and element. Target is the element which would be covered by a glass pane, element is the element the user interacts with (e.g. the modal dialog).
+ * @see _glassPaneTargets
+ */
+scout.Desktop.prototype.addGlassPaneTargetFilter = function(filter) {
+  this._glassPaneTargetFilters.push(filter);
+};
+
+scout.Desktop.prototype.removeGlassPaneTargetFilter = function(filter) {
+  scout.arrays.remove(this._glassPaneTargetFilters, filter);
+};
+
+/**
  * This 'deferred' object is used because popup windows are not immediately usable when they're opened.
  * That's why we must render the glass-pane of a popup window later. Which means, at the point in time
  * when its $container is created and ready for usage. To avoid race conditions we must also wait until
@@ -1264,3 +1284,35 @@
     clearBody: false
   });
 };
+
+/**
+ * Moves all the given overlays (popups, dialogs, message boxes etc.) before the target overlay and activates the focus context of the target overlay.
+ *
+ * @param overlaysToMove {HTMLElement[]} the overlays which should be moved before the target overlay
+ * @param $targetOverlay {$|HTMLElement} the overlay which should eventually be on top of the movable overlays
+ */
+scout.Desktop.prototype.moveOverlaysBehindAndFocus = function(overlaysToMove, $targetOverlay) {
+  $targetOverlay = $.ensure($targetOverlay);
+  $targetOverlay.nextAll().toArray()
+    .forEach(function(overlay) {
+      if (scout.arrays.containsAll(overlaysToMove, [overlay])) {
+        this.trigger('overlayMoveStart', {
+          overlay: overlay
+        });
+        $(overlay).insertBefore($targetOverlay);
+        this.trigger('overlayMoveEnd', {
+          overlay: overlay
+        });
+      }
+    }.bind(this));
+
+  // Activate the focus context of the form (will restore the previously focused field)
+  // This must not be done when the currently focused element is part of this dialog's DOM
+  // subtree, even if it has a separate focus context. Otherwise, the dialog would be
+  // (unnecessarily) activated, causing the current focus context to lose the focus.
+  // Example: editable table with a cell editor popup --> editor should keep the focus
+  // when the user clicks the clear icon ("x") inside the editor field.
+  if (!$targetOverlay.isOrHas($targetOverlay.activeElement())) {
+    this.session.focusManager.activateFocusContext($targetOverlay);
+  }
+};
diff --git a/org.eclipse.scout.rt.ui.html/src/main/js/scout/form/FormController.js b/org.eclipse.scout.rt.ui.html/src/main/js/scout/form/FormController.js
index f4bbb34..d426980 100644
--- a/org.eclipse.scout.rt.ui.html/src/main/js/scout/form/FormController.js
+++ b/org.eclipse.scout.rt.ui.html/src/main/js/scout/form/FormController.js
@@ -271,51 +271,41 @@
     return;
   }
 
+  var siblings = dialog.$container.nextAll().toArray();
+
   // Now the approach is to move all eligible siblings that are in the DOM after the given dialog.
   // It is important not to move the given dialog itself, because this would interfere with the further handling of the
-  // mousedown-DOM-event that triggerd this function.
-  var movableSiblings = dialog.$container.nextAll().toArray()
-    .filter(function(sibling) {
-      // siblings of a dialog are movable if they meet the following criteria:
-      // - they are forms (sibling forms of a dialog are always dialogs)
-      // - they are either
-      //     - not modal
-      //     - modal
-      //         - and not a descendant of the dialog to activate
-      //         - and their display parent is not the desktop
-      var siblingWidget = scout.widget(sibling);
-      return siblingWidget instanceof scout.Form &&
-        (!siblingWidget.modal ||
-          (!dialog.has(siblingWidget) && siblingWidget.displayParent !== this.session.desktop));
-    }, this);
+  // mousedown-DOM-event that triggered this function.
+  var movableSiblings = siblings.filter(function(sibling) {
+    var siblingWidget = scout.widget(sibling);
+    if (siblingWidget.displayParent === this.session.desktop && (siblingWidget.modal || siblingWidget instanceof scout.MessageBox || siblingWidget instanceof scout.FileChooser)) {
+      // Modal overlays with displayParent = desktop (= block the whole desktop) are never movable
+      // MessageBoxes and FileChooser are actually modal but don't have a modal property
+      return false;
+    }
 
-  // All descendants of the so far determined movableSiblings are moveable as well. (E.g. MessageBox, FileChooser)
-  var movableSiblingsDescendants = dialog.$container.nextAll().toArray()
-    .filter(function(sibling) {
-      return scout.arrays.find(movableSiblings, function(movableSibling) {
-        var siblingWidget = scout.widget(sibling);
-        return !(siblingWidget instanceof scout.Form) && // all movable forms are already captured by the filter above
-          scout.widget(movableSibling).has(siblingWidget);
-      });
+    if (siblingWidget instanceof scout.Form) {
+      // A form is movable if it is not modal
+      // Or if it is modal it must not belong to the dialog to activate
+      return !siblingWidget.modal || !dialog.has(siblingWidget);
+    }
+
+    // Accept the overlay if it is not a form and also not a child of a form (e.g. top level popups)
+    // Children of forms are collected later
+    return !siblingWidget.getForm();
+  }, this);
+
+  // All descendants of the so far determined movableSiblings are movable as well. (E.g. MessageBox, FileChooser)
+  var movableSiblingsDescendants = siblings.filter(function(sibling) {
+    return scout.arrays.find(movableSiblings, function(movableSibling) {
+      var siblingWidget = scout.widget(sibling);
+      return !(siblingWidget instanceof scout.Form) && // all movable forms are already captured by the filter above
+        scout.widget(movableSibling).has(siblingWidget);
     });
+  });
   movableSiblings = movableSiblings.concat(movableSiblingsDescendants);
 
-  dialog.$container.nextAll().toArray()
-    .forEach(function(sibling) {
-      if (scout.arrays.containsAll(movableSiblings, [sibling])) {
-        $(sibling).insertBefore(dialog.$container);
-      }
-    }.bind(this));
-
-  // Activate the focus context of the form (will restore the previously focused field)
-  // This must not be done when the currently focused element is part of this dialog's DOM
-  // subtree, even if it has a separate focus context. Otherwise, the dialog would be
-  // (unnecessarily) activated, causing the current focus context to lose the focus.
-  // Example: editable table with a cell editor popup --> editor should keep the focus
-  // when the user clicks the clear icon ("x") inside the editor field.
-  if (!dialog.$container.isOrHas(dialog.$container.activeElement())) {
-    this.session.focusManager.activateFocusContext(dialog.$container);
-  }
+  this.session.desktop.moveOverlaysBehindAndFocus(movableSiblings, dialog.$container);
 };
 
 /**
diff --git a/org.eclipse.scout.rt.ui.html/src/main/js/scout/iframe/IFrame.js b/org.eclipse.scout.rt.ui.html/src/main/js/scout/iframe/IFrame.js
index 4ee7d60..5c2db36 100644
--- a/org.eclipse.scout.rt.ui.html/src/main/js/scout/iframe/IFrame.js
+++ b/org.eclipse.scout.rt.ui.html/src/main/js/scout/iframe/IFrame.js
@@ -21,6 +21,7 @@
   this.wrapIframe = scout.device.isIosPlatform();
   this.$iframe = null;
   this._loadHandler = this._onLoad.bind(this);
+  this.mutationObserver = null;
 };
 scout.inherits(scout.IFrame, scout.Widget);
 
@@ -33,6 +34,25 @@
     this.$container = this.$iframe;
   }
   this.htmlComp = scout.HtmlComponent.install(this.$container, this.session);
+
+  this.$iframe.one('remove', function() {
+    if (!this.rendered || this.removing) {
+      return;
+    }
+    this.mutationObserver = new MutationObserver(this._onDomMutation.bind(this));
+    this.mutationObserver.observe(this.$iframe.document(true), {
+      subtree: true,
+      childList: true
+    });
+  }.bind(this));
+};
+
+scout.IFrame.prototype._remove = function() {
+  if (this.mutationObserver) {
+    this.mutationObserver.disconnect();
+    this.mutationObserver = null;
+  }
+  scout.IFrame.parent.prototype._remove.call(this);
 };
 
 /**
@@ -69,6 +89,22 @@
   }
 };
 
+scout.IFrame.prototype._onDomMutation = function(mutationList) {
+  mutationList.forEach(function(mutation) {
+    for (var i = 0; i < mutation.addedNodes.length; i++) {
+      var elem = mutation.addedNodes[i];
+      var $elem = $(elem);
+      if ($elem.isOrHas(this.$iframe)) {
+        this._onNodeAdded();
+      }
+    }
+  }, this);
+};
+
+scout.IFrame.prototype._onDomMutation = function() {
+  this._renderLocation();
+};
+
 scout.IFrame.prototype._onLoad = function(event) {
   if (!this.rendered) { // check needed, because this is an async callback
     return;